summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatija Čupić <matteeyah@gmail.com>2018-03-28 16:36:27 +0200
committerMatija Čupić <matteeyah@gmail.com>2018-03-28 16:36:27 +0200
commit490d18691cf9a0a3c1a4668a9da70f87932724e8 (patch)
tree4e95be717a06aef93574103b9a27bbd5184bff8c
parentbb0483dc2f9501461407766f74600e0f3d283686 (diff)
parentfffa19cbb623e42d2cea5369e3c853f1ee96033e (diff)
downloadgitlab-ce-490d18691cf9a0a3c1a4668a9da70f87932724e8.tar.gz
Merge branch 'master' into 42568-pipeline-empty-state
-rw-r--r--.babelrc26
-rw-r--r--.eslintignore5
-rw-r--r--.flayignore1
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab/merge_request_templates/Documentation.md27
-rw-r--r--.prettierignore5
-rw-r--r--.prettierrc11
-rw-r--r--.rubocop.yml3
-rw-r--r--CHANGELOG.md233
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile60
-rw-r--r--Gemfile.lock125
-rw-r--r--Gemfile.rails57
-rw-r--r--Gemfile.rails5.lock1219
-rw-r--r--Procfile1
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/behaviors/index.js3
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js (renamed from app/assets/javascripts/behaviors/copy_as_gfm.js)4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js (renamed from app/assets/javascripts/render_gfm.js)2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js (renamed from app/assets/javascripts/render_math.js)4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js (renamed from app/assets/javascripts/render_mermaid.js)6
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js4
-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/confirm_danger_modal.js57
-rw-r--r--app/assets/javascripts/dispatcher.js12
-rw-r--r--app/assets/javascripts/gl_dropdown.js2
-rw-r--r--app/assets/javascripts/gl_form.js6
-rw-r--r--app/assets/javascripts/groups/components/app.vue88
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue31
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue65
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue66
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue35
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue60
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue94
-rw-r--r--app/assets/javascripts/ide/components/editor_mode_dropdown.vue91
-rw-r--r--app/assets/javascripts/ide/components/ide.vue111
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue84
-rw-r--r--app/assets/javascripts/ide/components/ide_external_links.vue43
-rw-r--r--app/assets/javascripts/ide/components/ide_project_branches_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_project_tree.vue65
-rw-r--r--app/assets/javascripts/ide/components/ide_repo_tree.vue41
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue51
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue60
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue111
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue99
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue75
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue172
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue161
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue128
-rw-r--r--app/assets/javascripts/ide/components/repo_file_buttons.vue61
-rw-r--r--app/assets/javascripts/ide/components/repo_file_status_icon.vue39
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue42
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue98
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue61
-rw-r--r--app/assets/javascripts/ide/components/resizable_panel.vue88
-rw-r--r--app/assets/javascripts/ide/eventhub.js3
-rw-r--r--app/assets/javascripts/ide/ide_router.js117
-rw-r--r--app/assets/javascripts/ide/index.js33
-rw-r--r--app/assets/javascripts/ide/lib/common/disposable.js14
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js90
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js51
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js45
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js72
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff.js30
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js10
-rw-r--r--app/assets/javascripts/ide/lib/editor.js168
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js16
-rw-r--r--app/assets/javascripts/ide/lib/themes/gl_theme.js14
-rw-r--r--app/assets/javascripts/ide/monaco_loader.js16
-rw-r--r--app/assets/javascripts/ide/services/index.js55
-rw-r--r--app/assets/javascripts/ide/stores/actions.js121
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js146
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js49
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js93
-rw-r--r--app/assets/javascripts/ide/stores/getters.js30
-rw-r--r--app/assets/javascripts/ide/stores/index.js19
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js218
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/constants.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/getters.js24
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/index.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutation_types.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/mutations.js24
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/state.js6
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js43
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js106
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js26
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js83
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js23
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js38
-rw-r--r--app/assets/javascripts/ide/stores/state.js19
-rw-r--r--app/assets/javascripts/ide/stores/utils.js125
-rw-r--r--app/assets/javascripts/ide/stores/workers/files_decorator_worker.js101
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js131
-rw-r--r--app/assets/javascripts/main.js13
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue288
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue155
-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.js23
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue8
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue8
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue8
-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/edit/index.js2
-rw-r--r--app/assets/javascripts/performance_bar.js57
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue93
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue191
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue52
-rw-r--r--app/assets/javascripts/performance_bar/components/simple_metric.vue30
-rw-r--r--app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue20
-rw-r--r--app/assets/javascripts/performance_bar/index.js37
-rw-r--r--app/assets/javascripts/performance_bar/services/performance_bar_service.js45
-rw-r--r--app/assets/javascripts/performance_bar/stores/performance_bar_store.js39
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue8
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue8
-rw-r--r--app/assets/javascripts/profile/profile.js41
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js96
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue102
-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.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/deprecated_modal.vue (renamed from app/assets/javascripts/vue_shared/components/modal.vue)2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue8
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss33
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss109
-rw-r--r--app/assets/stylesheets/framework/header.scss108
-rw-r--r--app/assets/stylesheets/framework/images.scss41
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss1
-rw-r--r--app/assets/stylesheets/pages/boards.scss6
-rw-r--r--app/assets/stylesheets/pages/branches.scss21
-rw-r--r--app/assets/stylesheets/pages/events.scss9
-rw-r--r--app/assets/stylesheets/pages/labels.scss8
-rw-r--r--app/assets/stylesheets/pages/lint.scss21
-rw-r--r--app/assets/stylesheets/pages/notes.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss72
-rw-r--r--app/assets/stylesheets/pages/repo.scss359
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/performance_bar.scss43
-rw-r--r--app/controllers/admin/application_controller.rb14
-rw-r--r--app/controllers/ci/lints_controller.rb15
-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/variables_controller.rb2
-rw-r--r--app/controllers/ide_controller.rb6
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb26
-rw-r--r--app/controllers/projects/artifacts_controller.rb12
-rw-r--r--app/controllers/projects/ci/lints_controller.rb27
-rw-r--r--app/controllers/projects/jobs_controller.rb32
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb56
-rw-r--r--app/controllers/projects/milestones_controller.rb4
-rw-r--r--app/controllers/projects/pages_controller.rb22
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb17
-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/settings/ci_cd_controller.rb6
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/root_controller.rb4
-rw-r--r--app/finders/admin/projects_finder.rb1
-rw-r--r--app/helpers/application_helper.rb13
-rw-r--r--app/helpers/application_settings_helper.rb5
-rw-r--r--app/helpers/blob_helper.rb11
-rw-r--r--app/helpers/issuables_helper.rb7
-rw-r--r--app/helpers/preferences_helper.rb14
-rw-r--r--app/helpers/projects_helper.rb18
-rw-r--r--app/helpers/services_helper.rb23
-rw-r--r--app/mailers/emails/merge_requests.rb8
-rw-r--r--app/models/appearance.rb2
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/ci/build.rb27
-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.rb28
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb2
-rw-r--r--app/models/ci/variable.rb2
-rw-r--r--app/models/clusters/cluster.rb2
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/concerns/atomic_internal_id.rb46
-rw-r--r--app/models/concerns/avatarable.rb3
-rw-r--r--app/models/concerns/deployment_platform.rb22
-rw-r--r--app/models/concerns/nonatomic_internal_id.rb (renamed from app/models/concerns/internal_id.rb)2
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/event.rb10
-rw-r--r--app/models/group.rb10
-rw-r--r--app/models/internal_id.rb125
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/lfs_object.rb15
-rw-r--r--app/models/member.rb2
-rw-r--r--app/models/merge_request.rb29
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/notification_recipient.rb18
-rw-r--r--app/models/notification_setting.rb7
-rw-r--r--app/models/pages_domain.rb10
-rw-r--r--app/models/project.rb28
-rw-r--r--app/models/project_services/assembla_service.rb4
-rw-r--r--app/models/project_services/bamboo_service.rb12
-rw-r--r--app/models/project_services/buildkite_service.rb2
-rw-r--r--app/models/project_services/campfire_service.rb7
-rw-r--r--app/models/project_services/drone_ci_service.rb2
-rw-r--r--app/models/project_services/external_wiki_service.rb4
-rw-r--r--app/models/project_services/gemnasium_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb14
-rw-r--r--app/models/project_services/mock_ci_service.rb2
-rw-r--r--app/models/project_services/packagist_service.rb2
-rw-r--r--app/models/project_services/pivotaltracker_service.rb4
-rw-r--r--app/models/project_services/pushover_service.rb5
-rw-r--r--app/models/project_services/teamcity_service.rb12
-rw-r--r--app/models/redirect_route.rb28
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/route.rb26
-rw-r--r--app/models/service.rb23
-rw-r--r--app/models/upload.rb19
-rw-r--r--app/models/user.rb15
-rw-r--r--app/policies/protected_branch_policy.rb9
-rw-r--r--app/services/ci/create_pipeline_service.rb3
-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/merge_requests/refresh_service.rb8
-rw-r--r--app/services/notification_service.rb14
-rw-r--r--app/services/projects/destroy_service.rb6
-rw-r--r--app/services/projects/import_service.rb2
-rw-r--r--app/services/projects/update_pages_configuration_service.rb6
-rw-r--r--app/services/projects/update_pages_service.rb18
-rw-r--r--app/services/projects/update_service.rb10
-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/submit_usage_ping_service.rb5
-rw-r--r--app/services/verify_pages_domain_service.rb10
-rw-r--r--app/services/web_hook_service.rb16
-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.rb421
-rw-r--r--app/uploaders/personal_file_uploader.rb17
-rw-r--r--app/uploaders/records_uploads.rb3
-rw-r--r--app/validators/certificate_validator.rb2
-rw-r--r--app/validators/importable_url_validator.rb2
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml39
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml47
-rw-r--r--app/views/admin/application_settings/_form.html.haml391
-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/_pages.html.haml22
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml28
-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/_visibility_and_access.html.haml66
-rw-r--r--app/views/admin/application_settings/show.html.haml107
-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.haml2
-rw-r--r--app/views/ide/index.html.haml12
-rw-r--r--app/views/import/gitlab_projects/new.html.haml6
-rw-r--r--app/views/layouts/_mailer.html.haml10
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/header/_read_only_banner.html.haml7
-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/peek/_bar.html.haml12
-rw-r--r--app/views/peek/views/_gitaly.html.haml17
-rw-r--r--app/views/peek/views/_host.html.haml2
-rw-r--r--app/views/peek/views/_mysql2.html.haml4
-rw-r--r--app/views/peek/views/_pg.html.haml4
-rw-r--r--app/views/peek/views/_rblineprof.html.haml7
-rw-r--r--app/views/peek/views/_sql.html.haml14
-rw-r--r--app/views/projects/_last_push.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml4
-rw-r--r--app/views/projects/blob/_header.html.haml1
-rw-r--r--app/views/projects/branches/_branch.html.haml141
-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/user/_header.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml4
-rw-r--r--app/views/projects/environments/metrics.html.haml1
-rw-r--r--app/views/projects/issues/_issue.html.haml8
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml8
-rw-r--r--app/views/projects/pages/_https_only.html.haml10
-rw-r--r--app/views/projects/pages/show.html.haml3
-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/tree/_tree_header.html.haml4
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml8
-rw-r--r--app/views/shared/_service_settings.html.haml2
-rw-r--r--app/views/shared/boards/components/_board.html.haml2
-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/object_storage/background_move_worker.rb29
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb202
-rw-r--r--app/workers/object_storage_upload_worker.rb21
-rw-r--r--app/workers/repository_fork_worker.rb1
-rwxr-xr-xbin/rails15
-rwxr-xr-xbin/rake13
-rwxr-xr-xbin/setup50
-rwxr-xr-xbin/update29
-rw-r--r--changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml6
-rw-r--r--changelogs/unreleased-ee/4378-fix-cluster-js-not-running-on-update-page.yml5
-rw-r--r--changelogs/unreleased-ee/bvl-external-policy-classification.yml5
-rw-r--r--changelogs/unreleased/17203-add-missing-pagination-commit-diff-endpoint.yml5
-rw-r--r--changelogs/unreleased/17359-move-oauth-modules-to-auth-dir-structure.yml4
-rw-r--r--changelogs/unreleased/17500-mr-multiple-issues-oxford-comma.yml5
-rw-r--r--changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml5
-rw-r--r--changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml5
-rw-r--r--changelogs/unreleased/26039-Update-to-github-linguist5-3-x.yml5
-rw-r--r--changelogs/unreleased/26466-natural-sort-mrs.yml4
-rw-r--r--changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml5
-rw-r--r--changelogs/unreleased/29130-api-project-export.yml5
-rw-r--r--changelogs/unreleased/29497-pages-custom-domain-dns-verification.yml5
-rw-r--r--changelogs/unreleased/30665-add-email-button-to-new-issue-by-email.yml4
-rw-r--r--changelogs/unreleased/31114-internal-ids-are-not-atomic.yml5
-rw-r--r--changelogs/unreleased/32564-fix-double-system-closing-notes.yml5
-rw-r--r--changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml5
-rw-r--r--changelogs/unreleased/33570-slack-notify-default-branch.yml5
-rw-r--r--changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml5
-rw-r--r--changelogs/unreleased/35418-remove-underline-for-avatar.yml5
-rw-r--r--changelogs/unreleased/35530-teleporting-emoji.yml5
-rw-r--r--changelogs/unreleased/36847-update-update-toml-rb-to-1-0-0.yml5
-rw-r--r--changelogs/unreleased/37050-ext-issue-tracker.yml5
-rw-r--r--changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml5
-rw-r--r--changelogs/unreleased/38587-pipelines-empty-state.yml5
-rw-r--r--changelogs/unreleased/39444-make-margin-around-dropdown-dividers-4px.yml5
-rw-r--r--changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml5
-rw-r--r--changelogs/unreleased/39607-fix-avatar--vertical-align.yml5
-rw-r--r--changelogs/unreleased/40187-project-branch-dashboard-with-active-stale-branches.yml5
-rw-r--r--changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml5
-rw-r--r--changelogs/unreleased/40525-listing-user-activity-timeouts.yml5
-rw-r--r--changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml5
-rw-r--r--changelogs/unreleased/40623-fix-404-when-listing-archived-projects-in-a-group-where-all-projects-have-been-archived.yml4
-rw-r--r--changelogs/unreleased/40668-pages-domain-api-returns-404-when-using-a-specific-domain.yml5
-rw-r--r--changelogs/unreleased/40781-os-to-ce.yml5
-rw-r--r--changelogs/unreleased/40994-expose-features-as-ci-cd-variable.yml5
-rw-r--r--changelogs/unreleased/41616-api-issues-between-date.yml5
-rw-r--r--changelogs/unreleased/41719-mr-title-fix.yml5
-rw-r--r--changelogs/unreleased/41777-include-cycle-time-in-usage-ping.yml5
-rw-r--r--changelogs/unreleased/41851-enable-eslint-codeclimate.yml5
-rw-r--r--changelogs/unreleased/41899-api-endpoint-for-importing-a-project-export.yml5
-rw-r--r--changelogs/unreleased/41905_merge_request_and_issue_metrics.yml5
-rw-r--r--changelogs/unreleased/41949-move.yml5
-rw-r--r--changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml5
-rw-r--r--changelogs/unreleased/42044-osw-add-button-to-deploy-runner-to-kubernetes.yml5
-rw-r--r--changelogs/unreleased/42274-group-request-membership-long-too.yml5
-rw-r--r--changelogs/unreleased/42314-diff-file.yml5
-rw-r--r--changelogs/unreleased/42332-actionview-template-error-366-524-out-of-range.yml5
-rw-r--r--changelogs/unreleased/42431-add-auto-devops-and-clusters-button-to-projects.yml6
-rw-r--r--changelogs/unreleased/42434-allow-commits-endpoint-to-work-over-all-commits.yml5
-rw-r--r--changelogs/unreleased/42481-remove-notification-settings-left-projects.yml5
-rw-r--r--changelogs/unreleased/42509-fix-API-PUT-projects-fails-when-only-ci_config_path-is-specified.yml5
-rw-r--r--changelogs/unreleased/42545-milestion-quick-actions-for-groups.yml5
-rw-r--r--changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml5
-rw-r--r--changelogs/unreleased/42712_api_branches_add_search_param_20180207.yml5
-rw-r--r--changelogs/unreleased/42800-change-usage-of-avatar_icon.yml6
-rw-r--r--changelogs/unreleased/42814-fix-remove-source-branch-when-mwps.yml6
-rw-r--r--changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml5
-rw-r--r--changelogs/unreleased/42921-ci-charts-include-current-day.yml5
-rw-r--r--changelogs/unreleased/42922-environment-name.yml5
-rw-r--r--changelogs/unreleased/42923-close-issue.yml5
-rw-r--r--changelogs/unreleased/42929-hide-new-variable-values.yml5
-rw-r--r--changelogs/unreleased/42946-update-pipeline-cancel-tooltip-to-stop.yml5
-rw-r--r--changelogs/unreleased/43134-reduce-queries-pipelines-controller-show.yml5
-rw-r--r--changelogs/unreleased/43198-fix-settings-panel-expanding-when-fragment-hash-linked.yml5
-rw-r--r--changelogs/unreleased/43201-rename-repository-submit-button-disabled.yml5
-rw-r--r--changelogs/unreleased/43261-fix-import-from-url-name-collision-active-tab.yml6
-rw-r--r--changelogs/unreleased/43275-improve-variables-validation-message.yml5
-rw-r--r--changelogs/unreleased/43315-gpg-popover.yml5
-rw-r--r--changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml5
-rw-r--r--changelogs/unreleased/43334-reply-by-email-did-not-pick-up-unsubscribe-quick-action.yml5
-rw-r--r--changelogs/unreleased/43460-track-projects-a-user-interacted-with.yml5
-rw-r--r--changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml5
-rw-r--r--changelogs/unreleased/43489-display-runner-ip.yml5
-rw-r--r--changelogs/unreleased/43496-error-message-for-gke-clusters-persists-in-the-next-page.yml5
-rw-r--r--changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml5
-rw-r--r--changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml5
-rw-r--r--changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml5
-rw-r--r--changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml5
-rw-r--r--changelogs/unreleased/43603-ci-lint-support.yml5
-rw-r--r--changelogs/unreleased/43643-fix-mr-label-filtering.yml5
-rw-r--r--changelogs/unreleased/43771-improve-avatar-error-message.yml5
-rw-r--r--changelogs/unreleased/43780-add-a-paragraph-about-clusters-security-implications.yml5
-rw-r--r--changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml5
-rw-r--r--changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml5
-rw-r--r--changelogs/unreleased/43794-fix-domain-verification-validation-errors.yml5
-rw-r--r--changelogs/unreleased/43802-ensure-foreign-keys-on-clusters-applications.yml5
-rw-r--r--changelogs/unreleased/43829-update-ssh-addtion-text.yml5
-rw-r--r--changelogs/unreleased/43837-error-handle-in-updating-milestone-on-issue.yml5
-rw-r--r--changelogs/unreleased/43924-breadcrumbs-on-project-tags.yml5
-rw-r--r--changelogs/unreleased/43933-always-notify-mentions.yml6
-rw-r--r--changelogs/unreleased/44022-singular-1-diff.yml5
-rw-r--r--changelogs/unreleased/44024-fix-table-extra-column.yml5
-rw-r--r--changelogs/unreleased/44149-issue-comment-buttons.yml5
-rw-r--r--changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml5
-rw-r--r--changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml5
-rw-r--r--changelogs/unreleased/44280-fix-code-search.yml5
-rw-r--r--changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml5
-rw-r--r--changelogs/unreleased/44383-cleanup-framework-header.yml5
-rw-r--r--changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml5
-rw-r--r--changelogs/unreleased/44386-better-ux-for-long-name-branches.yml5
-rw-r--r--changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml5
-rw-r--r--changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml5
-rw-r--r--changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml5
-rw-r--r--changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml5
-rw-r--r--changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml5
-rw-r--r--changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml5
-rw-r--r--changelogs/unreleased/4826-geo-wikisyncservice-attempts-to-sync-projects.yml5
-rw-r--r--changelogs/unreleased/4826-github-import-wiki-fix-1.yml5
-rw-r--r--changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml5
-rw-r--r--changelogs/unreleased/ab-44467-remove-index.yml5
-rw-r--r--changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml5
-rw-r--r--changelogs/unreleased/add-indexes-to-todos-for-heavy-users-like-sean.yml5
-rw-r--r--changelogs/unreleased/add-query-counts-to-profiler-output.yml5
-rw-r--r--changelogs/unreleased/ajax-requests-in-performance-bar.yml5
-rw-r--r--changelogs/unreleased/an-network-controller-fix.yml5
-rw-r--r--changelogs/unreleased/an-workhorse-3-8-0.yml5
-rw-r--r--changelogs/unreleased/api-refs-for-commit.yml5
-rw-r--r--changelogs/unreleased/asciidoc_inter_document_cross_references.yml5
-rw-r--r--changelogs/unreleased/assignees-vue-component-missing-data-container.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml5
-rw-r--r--changelogs/unreleased/bvl-allow-maintainer-to-push.yml5
-rw-r--r--changelogs/unreleased/bvl-no-permanent-redirect.yml5
-rw-r--r--changelogs/unreleased/bvl-port-of-ee-translations.yml5
-rw-r--r--changelogs/unreleased/cache-refactor.yml5
-rw-r--r--changelogs/unreleased/ce-jej-github-project-service-for-ci.yml5
-rw-r--r--changelogs/unreleased/ce-jej-integrations-can-hide-trigger-checkboxes.yml6
-rw-r--r--changelogs/unreleased/change-strip-whitespace-from-username-input-42637.yml5
-rw-r--r--changelogs/unreleased/ci-pipeline-commit-lookup.yml5
-rw-r--r--changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml5
-rw-r--r--changelogs/unreleased/discussions-api.yml5
-rw-r--r--changelogs/unreleased/dm-dont-cache-nil-root-ref.yml5
-rw-r--r--changelogs/unreleased/dm-escape-commit-message.yml5
-rw-r--r--changelogs/unreleased/dm-go-get-api-token.yml5
-rw-r--r--changelogs/unreleased/dm-refs-contains-sha-encoding.yml5
-rw-r--r--changelogs/unreleased/dm-stuck-import-jobs-verify.yml5
-rw-r--r--changelogs/unreleased/docs-update-vue-naming-guidelines.yml5
-rw-r--r--changelogs/unreleased/dz-improve-app-settings-2.yml5
-rw-r--r--changelogs/unreleased/dz-namespace-id-not-null.yml5
-rw-r--r--changelogs/unreleased/dz-plugins-project-integrations.yml5
-rw-r--r--changelogs/unreleased/dz-system-hooks-plugins.yml5
-rw-r--r--changelogs/unreleased/ee-4862-verify-file-checksums.yml5
-rw-r--r--changelogs/unreleased/expose-commits-mr-api.yml5
-rw-r--r--changelogs/unreleased/feature--2848-display-time-tracking-totals-milestone-page.yml5
-rw-r--r--changelogs/unreleased/feature--43691-count-diff-note-calendar-activity.yml5
-rw-r--r--changelogs/unreleased/feature-26598-clear-button-ci-lint.yml4
-rw-r--r--changelogs/unreleased/feature-edit_pages_domain.yml5
-rw-r--r--changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml5
-rw-r--r--changelogs/unreleased/feature-include-custom-attributes-in-api.yml5
-rw-r--r--changelogs/unreleased/feature-oidc-groups-claim.yml4
-rw-r--r--changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml5
-rw-r--r--changelogs/unreleased/fix-40798-namespace-forking.yml5
-rw-r--r--changelogs/unreleased/fix-auth0-unsafe-login.yml5
-rw-r--r--changelogs/unreleased/fix-change-event-body-label-font-size.yml5
-rw-r--r--changelogs/unreleased/fix-dropzone-project-show.yml5
-rw-r--r--changelogs/unreleased/fix-new-project-path-input-overlapping.yml5
-rw-r--r--changelogs/unreleased/fix-squash-with-renamed-files.yml5
-rw-r--r--changelogs/unreleased/fix-template-project-visibility.yml5
-rw-r--r--changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml5
-rw-r--r--changelogs/unreleased/fj-28141-redirection-loop.yml5
-rw-r--r--changelogs/unreleased/fj-41174-projects-groups-badges-api.yml5
-rw-r--r--changelogs/unreleased/fj-42910-unauthenticated-limit-via-ssh.yml5
-rw-r--r--changelogs/unreleased/fl-refresh-btn.yml5
-rw-r--r--changelogs/unreleased/group-label-page-breadcrumb.yml5
-rw-r--r--changelogs/unreleased/ide-folder-button-path.yml5
-rw-r--r--changelogs/unreleased/ide-project-avatar-identicon.yml5
-rw-r--r--changelogs/unreleased/increase-unicorn-memory-killer-limits.yml5
-rw-r--r--changelogs/unreleased/issue-39885.yml5
-rw-r--r--changelogs/unreleased/issue_25542.yml (renamed from changelogs/unreleased/zj-move-opt-out-ruby-endpoints.yml)2
-rw-r--r--changelogs/unreleased/issue_31081.yml5
-rw-r--r--changelogs/unreleased/issue_38337.yml5
-rw-r--r--changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml5
-rw-r--r--changelogs/unreleased/jprovazn-issueref.yml6
-rw-r--r--changelogs/unreleased/jprovazn-scoped-limit.yml6
-rw-r--r--changelogs/unreleased/kp-label-select-vue.yml5
-rw-r--r--changelogs/unreleased/merge-requests-api-filter-by-branch.yml5
-rw-r--r--changelogs/unreleased/move-email-footer-info-to-single-line.yml5
-rw-r--r--changelogs/unreleased/mr-commit-optimization.yml5
-rw-r--r--changelogs/unreleased/oauth_generic_provider.yml4
-rw-r--r--changelogs/unreleased/optional-api-delimiter.yml5
-rw-r--r--changelogs/unreleased/osw-43951-single-batch-blob-request-to-gitaly.yml5
-rw-r--r--changelogs/unreleased/osw-stop-recalculating-merge-base-on-mr-loading.yml5
-rw-r--r--changelogs/unreleased/pages_force_https.yml5
-rw-r--r--changelogs/unreleased/poc-upload-hashing-path.yml5
-rw-r--r--changelogs/unreleased/proper-fix-for-artifacts-service.yml5
-rw-r--r--changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml5
-rw-r--r--changelogs/unreleased/refactor-move-assignees-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-filtered-search-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-issuable-time-tracker-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-sidebar-assignee-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-time-tracking-vue-components.yml5
-rw-r--r--changelogs/unreleased/remove-unnecessary-validate-project.yml5
-rw-r--r--changelogs/unreleased/replace_redcarpet_with_cmark.yml5
-rw-r--r--changelogs/unreleased/sh-cache-column-exists.yml5
-rw-r--r--changelogs/unreleased/sh-cache-table-exists.yml5
-rw-r--r--changelogs/unreleased/sh-cleanup-after-git-gc.yml5
-rw-r--r--changelogs/unreleased/sh-dashboard-sort-fix.yml5
-rw-r--r--changelogs/unreleased/sh-fix-geo-error-500-gpg-commit.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml5
-rw-r--r--changelogs/unreleased/sh-fix-otp-backup-code-invalidation.yml5
-rw-r--r--changelogs/unreleased/sh-make-prune-optional-in-git-fetch.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-admin-projects-page.yml5
-rw-r--r--changelogs/unreleased/tc-api-fix-expose_url.yml5
-rw-r--r--changelogs/unreleased/tc-re-add-read-only-banner.yml5
-rw-r--r--changelogs/unreleased/unassign-when-leaving.yml5
-rw-r--r--changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml5
-rw-r--r--changelogs/unreleased/update-unresolved-discussions-vue-component.yml5
-rw-r--r--changelogs/unreleased/upgrade-workhorse-4-0-0.yml5
-rw-r--r--changelogs/unreleased/winh-deprecate-old-modal.yml5
-rw-r--r--changelogs/unreleased/winh-new-modal-component.yml5
-rw-r--r--changelogs/unreleased/wip-new-mr-cmd.yml5
-rw-r--r--changelogs/unreleased/workhorse-gitaly-mandatory.yml5
-rw-r--r--changelogs/unreleased/zj-remote-repo-exists.yml5
-rw-r--r--changelogs/unreleased/zj-version-string-grouping-ci.yml5
-rw-r--r--config.ru4
-rw-r--r--config/application.rb2
-rw-r--r--config/boot.rb11
-rw-r--r--config/environment.rb8
-rw-r--r--config/environments/production.rb6
-rw-r--r--config/environments/test.rb8
-rw-r--r--config/gitlab.yml.example89
-rw-r--r--config/initializers/1_settings.rb31
-rw-r--r--config/initializers/6_validations.rb6
-rw-r--r--config/initializers/active_record_locking.rb108
-rw-r--r--config/initializers/application_controller_renderer.rb12
-rw-r--r--config/initializers/ar5_batching.rb72
-rw-r--r--config/initializers/ar5_pg_10_support.rb5
-rw-r--r--config/initializers/ar_native_database_types.rb11
-rw-r--r--config/initializers/carrierwave.rb12
-rw-r--r--config/initializers/devise.rb46
-rw-r--r--config/initializers/fog_google_https_private_urls.rb20
-rw-r--r--config/initializers/lograge.rb18
-rw-r--r--config/initializers/new_framework_defaults.rb29
-rw-r--r--config/karma.config.js8
-rw-r--r--config/routes.rb5
-rw-r--r--config/routes/ci.rb2
-rw-r--r--config/routes/project.rb6
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--config/spring.rb6
-rw-r--r--config/webpack.config.js98
-rw-r--r--db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb2
-rw-r--r--db/migrate/20161220141214_remove_dot_git_from_group_names.rb2
-rw-r--r--db/migrate/20161226122833_remove_dot_git_from_usernames.rb4
-rw-r--r--db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb8
-rw-r--r--db/migrate/20170601163708_add_artifacts_store_to_ci_build.rb10
-rw-r--r--db/migrate/20170703102400_add_stage_id_foreign_key_to_builds.rb12
-rw-r--r--db/migrate/20170713104829_add_foreign_key_to_merge_requests.rb12
-rw-r--r--db/migrate/20170825015534_add_file_store_to_lfs_objects.rb31
-rw-r--r--db/migrate/20170918072949_add_file_store_job_artifacts.rb10
-rw-r--r--db/migrate/20171214144320_add_store_column_to_uploads.rb12
-rw-r--r--db/migrate/20180102220145_add_pages_https_only_to_projects.rb9
-rw-r--r--db/migrate/20180109183319_change_default_value_for_pages_https_only.rb13
-rw-r--r--db/migrate/20180223144945_add_allow_local_requests_from_hooks_and_services_to_application_settings.rb18
-rw-r--r--db/migrate/20180305095250_create_internal_ids_table.rb15
-rw-r--r--db/migrate/20180320182229_add_indexes_for_user_activity_queries.rb40
-rw-r--r--db/migrate/20180323150945_add_push_to_merge_request_to_notification_settings.rb9
-rw-r--r--db/migrate/20180327101207_remove_index_from_events_table.rb18
-rw-r--r--db/post_migrate/20180220150310_remove_empty_extern_uid_auth0_identities.rb25
-rw-r--r--db/post_migrate/20180223124427_build_user_interacted_projects_table.rb10
-rw-r--r--db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb37
-rw-r--r--db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb38
-rw-r--r--db/schema.rb23
-rw-r--r--doc/README.md166
-rw-r--r--doc/administration/auth/jwt.md72
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/administration/job_artifacts.md122
-rw-r--r--doc/administration/job_traces.md42
-rw-r--r--doc/administration/monitoring/performance/performance_bar.md6
-rw-r--r--doc/administration/raketasks/uploads/migrate.md74
-rw-r--r--doc/administration/uploads.md209
-rw-r--r--doc/api/commits.md68
-rw-r--r--doc/api/notification_settings.md4
-rw-r--r--doc/api/search.md26
-rw-r--r--doc/ci/README.md3
-rw-r--r--doc/ci/caching/img/clear_runners_cache.pngbin0 -> 16029 bytes
-rw-r--r--doc/ci/caching/index.md518
-rw-r--r--doc/ci/docker/using_docker_build.md91
-rw-r--r--doc/ci/examples/README.md4
-rw-r--r--doc/ci/quick_start/README.md4
-rw-r--r--doc/ci/runners/README.md59
-rw-r--r--doc/ci/runners/img/shared_runner_ip_address.pngbin0 -> 69821 bytes
-rw-r--r--doc/ci/runners/img/specific_runner_ip_address.pngbin0 -> 42055 bytes
-rw-r--r--doc/ci/yaml/README.md119
-rw-r--r--doc/development/emails.md10
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/development/migration_style_guide.md21
-rw-r--r--doc/development/new_fe_guide/development/performance.md15
-rw-r--r--doc/img/devops_lifecycle.pngbin0 -> 65043 bytes
-rw-r--r--doc/install/kubernetes/index.md6
-rw-r--r--doc/integration/auth0.md7
-rw-r--r--doc/integration/omniauth.md1
-rw-r--r--doc/integration/saml.md6
-rw-r--r--doc/raketasks/README.md1
-rw-r--r--doc/topics/autodevops/index.md18
-rw-r--r--doc/user/gitlab_com/index.md262
-rw-r--r--doc/user/profile/preferences.md4
-rw-r--r--doc/user/project/clusters/index.md33
-rw-r--r--doc/user/project/index.md14
-rw-r--r--doc/user/project/integrations/prometheus.md30
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md15
-rw-r--r--doc/user/project/merge_requests/img/remove_source_branch_status.pngbin0 -> 32649 bytes
-rw-r--r--doc/user/project/merge_requests/index.md16
-rw-r--r--doc/user/project/merge_requests/maintainer_access.md13
-rw-r--r--doc/user/project/pipelines/schedules.md2
-rw-r--r--doc/workflow/lfs/lfs_administration.md116
-rw-r--r--doc/workflow/notifications.md3
-rw-r--r--lib/api/commits.rb14
-rw-r--r--lib/api/helpers.rb16
-rw-r--r--lib/api/helpers/internal_helpers.rb2
-rw-r--r--lib/api/job_artifacts.rb4
-rw-r--r--lib/api/jobs.rb2
-rw-r--r--lib/api/project_export.rb2
-rw-r--r--lib/api/projects.rb6
-rw-r--r--lib/api/protected_branches.rb11
-rw-r--r--lib/api/runner.rb3
-rw-r--r--lib/api/search.rb4
-rw-r--r--lib/api/services.rb2
-rw-r--r--lib/api/v3/builds.rb6
-rw-r--r--lib/api/v3/projects.rb6
-rw-r--r--lib/backup/repository.rb4
-rw-r--r--lib/banzai/filter/autolink_filter.rb19
-rw-r--r--lib/banzai/filter/issuable_state_filter.rb10
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb4
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/auth/database/authentication.rb2
-rw-r--r--lib/gitlab/auth/ldap/authentication.rb22
-rw-r--r--lib/gitlab/auth/o_auth/authentication.rb1
-rw-r--r--lib/gitlab/bare_repository_import/importer.rb2
-rw-r--r--lib/gitlab/checks/project_moved.rb20
-rw-r--r--lib/gitlab/ci/config.rb3
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb15
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb47
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/config.rb6
-rw-r--r--lib/gitlab/ci/pipeline/seed/base.rb21
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb52
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb51
-rw-r--r--lib/gitlab/ci/stage/seed.rb62
-rw-r--r--lib/gitlab/ci/trace/http_io.rb187
-rw-r--r--lib/gitlab/ci/trace/stream.rb2
-rw-r--r--lib/gitlab/ci/yaml_processor.rb44
-rw-r--r--lib/gitlab/conflict/file_collection.rb5
-rw-r--r--lib/gitlab/database/migration_helpers.rb105
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb2
-rw-r--r--lib/gitlab/diff/file.rb39
-rw-r--r--lib/gitlab/encoding_helper.rb2
-rw-r--r--lib/gitlab/git/conflict/file.rb16
-rw-r--r--lib/gitlab/git/conflict/parser.rb5
-rw-r--r--lib/gitlab/git/gitlab_projects.rb2
-rw-r--r--lib/gitlab/git/repository.rb15
-rw-r--r--lib/gitlab/git/storage/checker.rb2
-rw-r--r--lib/gitlab/git/storage/circuit_breaker.rb2
-rw-r--r--lib/gitlab/git/wiki.rb3
-rw-r--r--lib/gitlab/git_access.rb10
-rw-r--r--lib/gitlab/gitaly_client.rb4
-rw-r--r--lib/gitlab/gitaly_client/conflict_files_stitcher.rb2
-rw-r--r--lib/gitlab/gitaly_client/remote_service.rb11
-rw-r--r--lib/gitlab/gitaly_client/storage_settings.rb35
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb8
-rw-r--r--lib/gitlab/health_checks/fs_shards_check.rb2
-rw-r--r--lib/gitlab/http.rb11
-rw-r--r--lib/gitlab/middleware/read_only.rb2
-rw-r--r--lib/gitlab/omniauth_initializer.rb75
-rw-r--r--lib/gitlab/profiler.rb12
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/gitlab/proxy_http_connection_adapter.rb34
-rw-r--r--lib/gitlab/repo_path.rb4
-rw-r--r--lib/gitlab/setup_helper.rb2
-rw-r--r--lib/gitlab/shell.rb6
-rw-r--r--lib/gitlab/task_helpers.rb4
-rw-r--r--lib/gitlab/url_blocker.rb23
-rw-r--r--lib/gitlab/verify/lfs_objects.rb2
-rw-r--r--lib/gitlab/verify/uploads.rb2
-rw-r--r--lib/gitlab/workhorse.rb18
-rw-r--r--lib/mattermost/session.rb24
-rw-r--r--lib/microsoft_teams/notifier.rb5
-rw-r--r--lib/peek/views/host.rb9
-rw-r--r--lib/system_check/orphans/namespace_check.rb4
-rw-r--r--lib/system_check/orphans/repository_check.rb6
-rw-r--r--lib/tasks/gitlab/artifacts/migrate.rake25
-rw-r--r--lib/tasks/gitlab/check.rake8
-rw-r--r--lib/tasks/gitlab/cleanup.rake4
-rw-r--r--lib/tasks/gitlab/info.rake2
-rw-r--r--lib/tasks/gitlab/lfs/migrate.rake22
-rw-r--r--lib/tasks/gitlab/uploads/migrate.rake33
-rw-r--r--lib/tasks/haml-lint.rake9
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake4
-rw-r--r--package.json6
-rw-r--r--public/robots.txt1
-rw-r--r--qa/qa/page/merge_request/show.rb17
-rw-r--r--rubocop/cop/gitlab/httparty.rb62
-rw-r--r--rubocop/rubocop.rb1
-rw-r--r--scripts/frontend/frontend_script_utils.js8
-rw-r--r--scripts/frontend/prettier.js40
-rwxr-xr-xscripts/lint-doc.sh34
-rw-r--r--spec/controllers/admin/projects_controller_spec.rb10
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb89
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb133
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb14
-rw-r--r--spec/controllers/projects/ci/lints_controller_spec.rb123
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb4
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb63
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb13
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb37
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb4
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb18
-rw-r--r--spec/controllers/projects/pipelines_settings_controller_spec.rb28
-rw-r--r--spec/controllers/projects/protected_branches_controller_spec.rb97
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb56
-rw-r--r--spec/controllers/root_controller_spec.rb24
-rw-r--r--spec/factories/appearances.rb17
-rw-r--r--spec/factories/ci/job_artifacts.rb4
-rw-r--r--spec/factories/clusters/clusters.rb4
-rw-r--r--spec/factories/internal_ids.rb7
-rw-r--r--spec/factories/lfs_objects.rb4
-rw-r--r--spec/factories/pages_domains.rb48
-rw-r--r--spec/factories/redirect_routes.rb9
-rw-r--r--spec/factories/uploads.rb5
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb15
-rw-r--r--spec/features/admin/admin_settings_spec.rb121
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb2
-rw-r--r--spec/features/projects/ci/lint_spec.rb (renamed from spec/features/ci_lint_spec.rb)8
-rw-r--r--spec/features/projects/hook_logs/user_reads_log_spec.rb21
-rw-r--r--spec/features/projects/pages_spec.rb91
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb4
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb53
-rw-r--r--spec/features/projects/tree/create_file_spec.rb43
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb14
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb51
-rw-r--r--spec/features/read_only_spec.rb25
-rw-r--r--spec/features/user_can_display_performance_bar_spec.rb10
-rw-r--r--spec/finders/clusters_finder_spec.rb2
-rw-r--r--spec/helpers/preferences_helper_spec.rb4
-rw-r--r--spec/initializers/6_validations_spec.rb14
-rw-r--r--spec/initializers/fog_google_https_private_urls_spec.rb24
-rw-r--r--spec/initializers/settings_spec.rb2
-rw-r--r--spec/javascripts/behaviors/copy_as_gfm_spec.js2
-rw-r--r--spec/javascripts/ci_variable_list/native_form_variable_list_spec.js2
-rw-r--r--spec/javascripts/fixtures/gl_dropdown.html.haml3
-rw-r--r--spec/javascripts/gl_dropdown_spec.js25
-rw-r--r--spec/javascripts/helpers/vue_component_helper.js3
-rw-r--r--spec/javascripts/ide/components/changed_file_icon_spec.js45
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/actions_spec.js35
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js28
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_item_spec.js85
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_spec.js53
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js130
-rw-r--r--spec/javascripts/ide/components/ide_context_bar_spec.js37
-rw-r--r--spec/javascripts/ide/components/ide_external_links_spec.js43
-rw-r--r--spec/javascripts/ide/components/ide_project_tree_spec.js39
-rw-r--r--spec/javascripts/ide/components/ide_repo_tree_spec.js43
-rw-r--r--spec/javascripts/ide/components/ide_side_bar_spec.js42
-rw-r--r--spec/javascripts/ide/components/ide_spec.js41
-rw-r--r--spec/javascripts/ide/components/new_dropdown/index_spec.js84
-rw-r--r--spec/javascripts/ide/components/new_dropdown/modal_spec.js82
-rw-r--r--spec/javascripts/ide/components/new_dropdown/upload_spec.js87
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js173
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js137
-rw-r--r--spec/javascripts/ide/components/repo_file_buttons_spec.js47
-rw-r--r--spec/javascripts/ide/components/repo_file_spec.js80
-rw-r--r--spec/javascripts/ide/components/repo_loading_file_spec.js63
-rw-r--r--spec/javascripts/ide/components/repo_tab_spec.js165
-rw-r--r--spec/javascripts/ide/components/repo_tabs_spec.js81
-rw-r--r--spec/javascripts/ide/helpers.js22
-rw-r--r--spec/javascripts/ide/lib/common/disposable_spec.js44
-rw-r--r--spec/javascripts/ide/lib/common/model_manager_spec.js129
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js113
-rw-r--r--spec/javascripts/ide/lib/decorations/controller_spec.js139
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js196
-rw-r--r--spec/javascripts/ide/lib/diff/diff_spec.js80
-rw-r--r--spec/javascripts/ide/lib/editor_options_spec.js11
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js193
-rw-r--r--spec/javascripts/ide/monaco_loader_spec.js15
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js421
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js172
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js306
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js55
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js505
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js128
-rw-r--r--spec/javascripts/ide/stores/modules/commit/mutations_spec.js42
-rw-r--r--spec/javascripts/ide/stores/mutations/branch_spec.js18
-rw-r--r--spec/javascripts/ide/stores/mutations/file_spec.js157
-rw-r--r--spec/javascripts/ide/stores/mutations/tree_spec.js69
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js79
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js66
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js3
-rw-r--r--spec/javascripts/lib/utils/text_markdown_spec.js10
-rw-r--r--spec/javascripts/merge_request_notes_spec.js3
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js1
-rw-r--r--spec/javascripts/monitoring/dashboard_state_spec.js46
-rw-r--r--spec/javascripts/notes/components/diff_file_header_spec.js2
-rw-r--r--spec/javascripts/notes/components/diff_with_note_spec.js2
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js2
-rw-r--r--spec/javascripts/notes_spec.js313
-rw-r--r--spec/javascripts/pages/labels/components/promote_label_modal_spec.js2
-rw-r--r--spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js2
-rw-r--r--spec/javascripts/performance_bar/components/detailed_metric_spec.js80
-rw-r--r--spec/javascripts/performance_bar/components/performance_bar_app_spec.js88
-rw-r--r--spec/javascripts/performance_bar/components/request_selector_spec.js47
-rw-r--r--spec/javascripts/performance_bar/components/simple_metric_spec.js47
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js89
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js6
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js5
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js33
-rw-r--r--spec/javascripts/vue_shared/components/deprecated_modal_spec.js (renamed from spec/javascripts/vue_shared/components/modal_spec.js)6
-rw-r--r--spec/javascripts/vue_shared/components/markdown/toolbar_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js4
-rw-r--r--spec/lib/backup/manager_spec.rb4
-rw-r--r--spec/lib/backup/repository_spec.rb2
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb21
-rw-r--r--spec/lib/banzai/filter/issuable_state_filter_spec.rb8
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb2
-rw-r--r--spec/lib/gitlab/auth_spec.rb14
-rw-r--r--spec/lib/gitlab/bare_repository_import/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/checks/project_moved_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb153
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb242
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb133
-rw-r--r--spec/lib/gitlab/ci/stage/seed_spec.rb83
-rw-r--r--spec/lib/gitlab/ci/trace/http_io_spec.rb315
-rw-r--r--spec/lib/gitlab/ci/trace_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb686
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb137
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb12
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb5
-rw-r--r--spec/lib/gitlab/git/conflict/file_spec.rb50
-rw-r--r--spec/lib/gitlab/git/conflict/parser_spec.rb7
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb5
-rw-r--r--spec/lib/gitlab/git_access_spec.rb59
-rw-r--r--spec/lib/gitlab/gitaly_client/remote_service_spec.rb10
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb6
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb6
-rw-r--r--spec/lib/gitlab/http_spec.rb49
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml3
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb65
-rw-r--r--spec/lib/gitlab/profiler_spec.rb24
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb22
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb4
-rw-r--r--spec/lib/gitlab/shell_spec.rb4
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb45
-rw-r--r--spec/lib/gitlab/verify/lfs_objects_spec.rb16
-rw-r--r--spec/lib/gitlab/verify/uploads_spec.rb16
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb11
-rw-r--r--spec/lib/mattermost/command_spec.rb5
-rw-r--r--spec/lib/mattermost/session_spec.rb2
-rw-r--r--spec/lib/mattermost/team_spec.rb5
-rw-r--r--spec/migrations/remove_dot_git_from_usernames_spec.rb4
-rw-r--r--spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb22
-rw-r--r--spec/migrations/remove_empty_fork_networks_spec.rb4
-rw-r--r--spec/models/ci/build_spec.rb59
-rw-r--r--spec/models/ci/job_artifact_spec.rb44
-rw-r--r--spec/models/ci/pipeline_spec.rb339
-rw-r--r--spec/models/concerns/issuable_spec.rb2
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/internal_id_spec.rb106
-rw-r--r--spec/models/issue_spec.rb8
-rw-r--r--spec/models/lfs_object_spec.rb85
-rw-r--r--spec/models/merge_request_spec.rb13
-rw-r--r--spec/models/namespace_spec.rb2
-rw-r--r--spec/models/pages_domain_spec.rb146
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb5
-rw-r--r--spec/models/project_spec.rb51
-rw-r--r--spec/models/repository_spec.rb26
-rw-r--r--spec/models/route_spec.rb159
-rw-r--r--spec/models/user_spec.rb47
-rw-r--r--spec/policies/protected_branch_policy_spec.rb22
-rw-r--r--spec/presenters/project_presenter_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb29
-rw-r--r--spec/requests/api/internal_spec.rb6
-rw-r--r--spec/requests/api/jobs_spec.rb84
-rw-r--r--spec/requests/api/pages_domains_spec.rb14
-rw-r--r--spec/requests/api/projects_spec.rb15
-rw-r--r--spec/requests/api/protected_branches_spec.rb34
-rw-r--r--spec/requests/api/runner_spec.rb70
-rw-r--r--spec/requests/api/search_spec.rb48
-rw-r--r--spec/requests/api/templates_spec.rb2
-rw-r--r--spec/requests/api/v3/builds_spec.rb32
-rw-r--r--spec/requests/api/v3/templates_spec.rb2
-rw-r--r--spec/requests/git_http_spec.rb28
-rw-r--r--spec/requests/lfs_http_spec.rb208
-rw-r--r--spec/rubocop/cop/gitlab/httparty_spec.rb74
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb1
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/retry_build_service_spec.rb3
-rw-r--r--spec/services/clusters/create_service_spec.rb2
-rw-r--r--spec/services/groups/transfer_service_spec.rb42
-rw-r--r--spec/services/issues/move_service_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb21
-rw-r--r--spec/services/notification_service_spec.rb51
-rw-r--r--spec/services/projects/create_service_spec.rb2
-rw-r--r--spec/services/projects/fork_service_spec.rb2
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/projects/update_service_spec.rb23
-rw-r--r--spec/services/protected_branches/create_service_spec.rb13
-rw-r--r--spec/services/protected_branches/destroy_service_spec.rb30
-rw-r--r--spec/services/protected_branches/update_service_spec.rb11
-rw-r--r--spec/services/protected_tags/destroy_service_spec.rb17
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/services/verify_pages_domain_service_spec.rb19
-rw-r--r--spec/services/web_hook_service_spec.rb14
-rw-r--r--spec/spec_helper.rb20
-rw-r--r--spec/support/gitaly.rb6
-rw-r--r--spec/support/http_io/http_io_helpers.rb64
-rw-r--r--spec/support/shared_examples/controllers/variables_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_spec.rb40
-rw-r--r--spec/support/shared_examples/uploaders/object_storage_shared_examples.rb126
-rw-r--r--spec/support/stored_repositories.rb2
-rw-r--r--spec/support/stub_configuration.rb8
-rw-r--r--spec/support/stub_object_storage.rb48
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--spec/tasks/gitlab/artifacts/migrate_rake_spec.rb118
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb15
-rw-r--r--spec/tasks/gitlab/cleanup_rake_spec.rb7
-rw-r--r--spec/tasks/gitlab/git_rake_spec.rb7
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb10
-rw-r--r--spec/tasks/gitlab/lfs/migrate_rake_spec.rb37
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/uploads/migrate_rake_spec.rb28
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb22
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb27
-rw-r--r--spec/uploaders/file_mover_spec.rb6
-rw-r--r--spec/uploaders/file_uploader_spec.rb61
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb4
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb35
-rw-r--r--spec/uploaders/legacy_artifact_uploader_spec.rb14
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb58
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb22
-rw-r--r--spec/uploaders/object_storage_spec.rb630
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb22
-rw-r--r--spec/views/projects/ci/lints/show.html.haml_spec.rb (renamed from spec/views/ci/lints/show.html.haml_spec.rb)32
-rw-r--r--spec/views/projects/diffs/_stats.html.haml_spec.rb56
-rw-r--r--spec/views/projects/services/_form.haml_spec.rb46
-rw-r--r--spec/workers/object_storage_upload_worker_spec.rb108
-rw-r--r--vendor/assets/javascripts/peek.js86
-rw-r--r--yarn.lock10
976 files changed, 25619 insertions, 6040 deletions
diff --git a/.babelrc b/.babelrc
index b93bef72de1..8cf07b73420 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,20 +1,20 @@
{
- "presets": [
- ["latest", { "es2015": { "modules": false } }],
- "stage-2"
- ],
+ "presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
"env": {
"coverage": {
"plugins": [
- ["istanbul", {
- "exclude": [
- "spec/javascripts/**/*",
- "app/assets/javascripts/locale/**/app.js"
- ]
- }],
- ["transform-define", {
- "process.env.BABEL_ENV": "coverage"
- }]
+ [
+ "istanbul",
+ {
+ "exclude": ["spec/javascripts/**/*", "app/assets/javascripts/locale/**/app.js"]
+ }
+ ],
+ [
+ "transform-define",
+ {
+ "process.env.BABEL_ENV": "coverage"
+ }
+ ]
]
}
}
diff --git a/.eslintignore b/.eslintignore
index 1623b996213..33a8186fade 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,11 +1,12 @@
+/app/assets/javascripts/locale/**/app.js
+/config/
/builds/
/coverage/
/coverage-javascript/
/node_modules/
/public/
+/scripts/
/tmp/
/vendor/
karma.config.js
webpack.config.js
-svg.config.js
-/app/assets/javascripts/locale/**/app.js
diff --git a/.flayignore b/.flayignore
index 87cb3507b05..3d69bb2c985 100644
--- a/.flayignore
+++ b/.flayignore
@@ -8,3 +8,4 @@ lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb
lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
+lib/gitlab/workhorse.rb
diff --git a/.gitignore b/.gitignore
index 447fb71bd64..e9ff0048c1c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,6 +50,7 @@ eslint-report.html
/db/data.yml
/doc/code/*
/dump.rdb
+/jsconfig.json
/log/*.log*
/node_modules/
/nohup.out
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 724e37141d6..70f41e4dc98 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -544,7 +544,7 @@ migration:path-mysql:
.db-rollback: &db-rollback
<<: *dedicated-no-docs-pull-cache-job
script:
- - bundle exec rake db:rollback STEP=119
+ - bundle exec rake db:migrate VERSION=20170523121229
- bundle exec rake db:migrate
db:rollback-pg:
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index 102eb7e7953..da38a703c3c 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -1,16 +1,29 @@
-See the general Documentation guidelines http://docs.gitlab.com/ce/development/doc_styleguide.html
+<!--See the general Documentation guidelines https://docs.gitlab.com/ce/development/writing_documentation.html -->
## What does this MR do?
-(briefly describe what this MR is about)
+<!-- Briefly describe what this MR is about -->
+
+## Related issues
+
+<!-- Mention the issue(s) this MR closes or is related to -->
+
+Closes
## Moving docs to a new location?
-See the guidelines: http://docs.gitlab.com/ce/development/doc_styleguide.html#changing-document-location
+Read the guidelines:
+https://docs.gitlab.com/ce/development/writing_documentation.html#changing-document-location
-- [ ] Make sure the old link is not removed and has its contents replaced with a link to the new location.
+- [ ] Make sure the old link is not removed and has its contents replaced with
+ a link to the new location.
- [ ] Make sure internal links pointing to the document in question are not broken.
-- [ ] Search and replace any links referring to old docs in GitLab Rails app, specifically under the `app/views/` directory.
-- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/doc_styleguide.html#redirections-for-pages-with-disqus-comments) to the new document if there are any Disqus comments on the old document thread.
-- [ ] If working on CE, submit an MR to EE with the changes as well.
+- [ ] Search and replace any links referring to old docs in GitLab Rails app,
+ specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
+- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments)
+ to the new document if there are any Disqus comments on the old document thread.
+- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
+ with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee).
- [ ] Ping one of the technical writers for review.
+
+/label ~Documentation
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000000..b674ccd50cf
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,5 @@
+/app/assets/javascripts/locale/**/app.js
+/node_modules/
+/public/
+/vendor/
+/tmp/
diff --git a/.prettierrc b/.prettierrc
index a20502b7f06..3384551aea5 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,4 +1,13 @@
{
+ "printWidth": 100,
"singleQuote": true,
- "trailingComma": "all"
+ "trailingComma": "es5",
+ "overrides": [
+ {
+ "files": ["**/app/**/*", "**/spec/**/*"],
+ "options": {
+ "trailingComma": "all"
+ }
+ }
+ ]
}
diff --git a/.rubocop.yml b/.rubocop.yml
index 14840ddd262..0582bfe8d70 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -118,6 +118,9 @@ Gitlab/ModuleWithInstanceVariables:
- spec/support/**/*.rb
- features/steps/**/*.rb
+Gitlab/HTTParty:
+ Enabled: true
+
GitlabSecurity/PublicSend:
Enabled: true
Exclude:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c64e68967e..adb0ec9f5b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,222 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.6.1 (2018-03-27)
+
+### Security (1 change)
+
+- Bump rails-html-sanitizer to 1.0.4.
+
+### Fixed (2 changes)
+
+- Prevent auto-retry AccessDenied error from stopping transition to failed. !17862
+- Fix 500 error when trying to resolve non-ASCII conflicts in the editor. !17962
+
+### Performance (1 change)
+
+- Add indexes for user activity queries. !17890
+
+### Other (1 change)
+
+- Add documentation for runner IP address (#44232). !17837
+
+
+## 10.6.0 (2018-03-22)
+
+### Security (4 changes)
+
+- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
+- Ensure that OTP backup codes are always invalidated.
+- Add verification for GitLab Pages custom domains.
+- Fix GitLab Auth0 integration signing in the wrong user.
+
+### Fixed (75 changes, 17 of them are from the community)
+
+- Ensure users cannot create environments with leading or trailing slashes (Fixes #39885). !15273
+- Fix new project path input overlapping. !16755 (George Tsiolis)
+- Respect description and visibility when creating project from template. !16820 (George Tsiolis)
+- Remove user notification settings for groups and projects when user leaves. !16906 (Jacopo Beschi @jacopo-beschi)
+- Fix Teleporting Emoji. !16963 (Jared Deckard <jared.deckard@gmail.com>)
+- Fix duplicate system notes when merging a merge request. !17035
+- Fix breadcrumb on labels page for groups. !17045 (Onuwa Nnachi Isaac)
+- Fix user avatar's vertical align on the issues and merge requests pages. !17072 (Laszlo Karpati)
+- Fix settings panels not expanding when fragment hash linked. !17074
+- Fix 404 when listing archived projects in a group where all projects have been archived. !17077 (Ashley Dumaine)
+- Allow to call PUT /projects/:id API with only ci_config_path specified. !17105 (Laszlo Karpati)
+- Fix long list of recipients on group request membership email. !17121 (Jacopo Beschi @jacopo-beschi)
+- Remove duplicated error message on duplicate variable validation. !17135
+- Keep "Import project" tab/form active when validation fails trying to import "Repo by URL". !17136
+- Fixed bug with unauthenticated requests through git ssh. !17149
+- Allows project rename after validation error. !17150
+- Fix "Remove source branch" button in Merge request widget during merge when pipeline succeeds state. !17192
+- Add missing pagination on the commit diff endpoint. !17203 (Maxime Roussin-Bélanger)
+- Fix get a single pages domain when project path contains a period. !17206 (Travis Miller)
+- remove avater underline. !17219 (Ken Ding)
+- Allows the usage of /milestone quick action for group milestones. !17239 (Jacopo Beschi @jacopo-beschi)
+- Encode branch name as binary before creating a RPC request to copy attributes. !17291
+- Restart Unicorn and Sidekiq when GRPC throws 14:Endpoint read failed. !17293
+- Do not persist Google Project verification flash errors after a page reload. !17299
+- Ensure group issues and merge requests pages show results from subgroups when there are no results from the current group. !17312
+- Prevent trace artifact migration to incur data loss. !17313
+- Fixes gpg popover layout. !17323
+- Return a 404 instead of 403 if the repository does not exist on disk. !17341
+- Fix Slack/Mattermost notifications not respecting `notify_only_default_branch` setting for pushes. !17345
+- Fix Group labels load failure when there are duplicate labels present. !17353
+- Allow Prometheus application to be installed from Cluster applications. !17372
+- Fixes Prometheus admin configuration page. !17377
+- Enable filtering MR list based on clicked label in MR sidebar. !17390
+- Fix code and wiki search results pages when non-ASCII text is displayed. !17413
+- Count comments on diffs and discussions as contributions for the contributions calendar. !17418 (Riccardo Padovani)
+- Add Assignees vue component missing data container. !17426 (George Tsiolis)
+- Update tooltip on pipeline cancel to Stop (#42946). !17444
+- Removing the two factor check when the user sets a new password. !17457
+- Fix quick actions for users who cannot update issues and merge requests. !17482
+- Stop loading spinner on error of milestone update on issue. !17507 (Takuya Noguchi)
+- Set margins around dropdown dividers to 4px. !17517
+- Fix pages flaky failure by reloading stale object. !17522
+- Remove extra breadcrumb on tags. !17562 (Takuya Noguchi)
+- Fix missing uploads after group transfer. !17658
+- Fix markdown table showing extra column. !17669
+- Ensure the API returns https links when https is configured. !17681
+- Sanitize extra blank spaces used when uploading a SSH key. !40552
+- Render htmlentities correctly for links not supported by Rinku.
+- Keep link when redacting unauthorized object links.
+- Handle empty state in Pipelines page.
+- Revert Project.public_or_visible_to_user changes and only apply to snippets.
+- Release libgit2 cache and open file descriptors after `git gc` run.
+- Fix project dashboard showing the wrong timestamps.
+- Fix "Can't modify frozen hash" error when project is destroyed.
+- Fix Error 500 when viewing a commit with a GPG signature in Geo.
+- Don't error out in system hook if user has `nil` datetime columns.
+- Remove double caching of Repository#empty?.
+- Don't delete todos or unassign issues and MRs when a user leaves a project.
+- Don't cache a nil repository root ref to prevent caching issues.
+- Escape HTML entities in commit messages.
+- Verify project import status again before marking as failed.
+- [GitHub Import] Create an empty wiki if wiki import failed.
+- Create empty wiki when import from GitLab and wiki is not there.
+- Make sure wiki exists when it's enabled.
+- Fix broken loading state for close issue button.
+- Fix code and wiki search results when filename is non-ASCII.
+- Fix file upload on project show page.
+- Fix squashing when a file is renamed.
+- Show loading button inline in refresh button in MR widget.
+- Fix close button on issues not working on mobile.
+- Adds tooltip in environment names to increase readability.
+- Fixed issue edit shortcut not opening edit form.
+- Fix 500 error being shown when diff has context marker with invalid encoding.
+- Render modified icon for moved file in changes dropdown.
+- Remember assignee when moving an issue.
+
+### Changed (16 changes, 9 of them are from the community)
+
+- Allow including custom attributes in API responses. !16526 (Markus Koller)
+- Apply new default and inline label design. !16956 (George Tsiolis)
+- Remove whitespace from the username/email sign in form field. !17020 (Peter lauck)
+- CI charts now include the current day. !17032 (Dakkaron)
+- Hide CI secret variable values after saving. !17044
+- Add new modal Vue component. !17108
+- Asciidoc now support inter-document cross references between files in repository. !17125 (Turo Soisenniemi)
+- Update issue closing pattern to allow variations in punctuation. !17198 (Vicky Chijwani)
+- Add a button to deploy a runner to a Kubernetes cluster in the settings page. !17278
+- Pages custom domain: allow update of key/certificate. !17376 (rfwatson)
+- Clear the Labels dropdown search filter after a selection is made. !17393 (Andrew Torres)
+- Hook data for pipelines includes detailed_status. !17607
+- Avoid showing unnecessary Trigger checkboxes for project Integrations with only one event. !17607
+- Display a link to external issue tracker when enabled.
+- Allow token authentication on go-get request.
+- Update SSH key link to include existing keys. (Brendan O'Leary)
+
+### Performance (24 changes, 5 of them are from the community)
+
+- Add catch-up background migration to migrate pipeline stages. !15741
+- Move BoardNewIssue vue component. !16947 (George Tsiolis)
+- Move IssuableTimeTracker vue component. !16948 (George Tsiolis)
+- Move RecentSearchesDropdownContent vue component. !16951 (George Tsiolis)
+- Move Assignees vue component. !16952 (George Tsiolis)
+- Improve performance of pipeline page by reducing DB queries. !17168
+- Store sha256 checksum to job artifacts. !17354
+- Move SidebarAssignees vue component. !17398 (George Tsiolis)
+- Improve database response time for user activity listing. !17454
+- Use persisted/memoized value for MRs shas instead of doing git lookups. !17555
+- Cache MergeRequests can_be_resolved_in_ui? git operations. !17589
+- Prevent the graphs page from generating unnecessary Gitaly requests. !37602
+- Use a user object in ApplicationHelper#avatar_icon where possible to avoid N+1 queries. !42800
+- Submit a single batch blob RPC to Gitaly per HTTP request when viewing diffs.
+- Avoid re-fetching merge-base SHA from Gitaly unnecessarily.
+- Don't use ProjectsFinder in TodosFinder.
+- Adding missing indexes on taggings table.
+- Add index on section_name_id on ci_build_trace_sections table.
+- Cache column_exists? for application settings.
+- Cache table_exists?('application_settings') to reduce repeated schema reloads.
+- Make --prune a configurable parameter in fetching a git remote.
+- Fix timeouts loading /admin/projects page.
+- Add partial indexes on todos to handle users with many todos.
+- Optimize search queries on the search page by setting a limit for matching records in project scope.
+
+### Added (30 changes, 9 of them are from the community)
+
+- Add CommonMark markdown engine (experimental). !14835 (blackst0ne)
+- API: Get references a commit is pushed to. !15026 (Robert Schilling)
+- Add overview of branches and a filter for active/stale branches. !15402 (Takuya Noguchi)
+- Add project export API. !15860 (Travis Miller)
+- expose more metrics in merge requests api. !16589 (haseebeqx)
+- #28481: Display time tracking totals on milestone page. !16753 (Riccardo Padovani)
+- Add a button on the project page to set up a Kubernetes cluster and enable Auto DevOps. !16900
+- Include cycle time in usage ping data. !16973
+- Add ability to use external plugins as an alternative to system hooks. !17003
+- Add search param to Branches API. !17005 (bunufi)
+- API endpoint for importing a project export. !17025
+- Display ingress IP address in the Kubernetes page. !17052
+- Implemented badge API endpoints. !17082
+- Allow installation of GitLab Runner with a single click. !17134
+- Allow commits endpoint to work over all commits of a repository. !17182
+- Display Runner IP Address. !17286
+- Add archive feature to trace. !17314
+- Allow maintainers to push to forks of their projects when a merge request is open. !17395
+- Foreground verification of uploads and LFS objects. !17402
+- Adds updated_at filter to issues and merge_requests API. !17417 (Jacopo Beschi @jacopo-beschi)
+- Port /wip quick action command to Merge Request creation (on description). !17463 (Adam Pahlevi)
+- Add a paragraph about security implications on Cluster's page. !17486
+- Add plugins list to the system hooks page. !17518
+- Enable privileged mode for GitLab Runner. !17528
+- Expose GITLAB_FEATURES as CI/CD variable (fixes #40994).
+- Upgrade GitLab Workhorse to 4.0.0.
+- Allow CI/CD Jobs being grouped on version strings.
+- Add discussions API for Issues and Snippets.
+- Add one group board to Libre.
+- Add support for filtering by source and target branch to merge requests API.
+
+### Other (18 changes, 7 of them are from the community)
+
+- Group MRs on issue page by project and namespace. !8494 (Jeff Stubler)
+- Make oauth provider login generic. !8809 (Horatiu Eugen Vlad)
+- Add email button to new issue by email. !10942 (Islam Wazery)
+- Update vue component naming guidelines. !17018 (George Tsiolis)
+- Added new design for promotion modals. !17197
+- Update to github-linguist 5.3.x. !17241 (Ken Ding)
+- update toml-rb to 1.0.0. !17259 (Ken Ding)
+- Keep track of projects a user interacted with. !17327
+- Moved o_auth/saml/ldap modules under gitlab/auth. !17359 (Horatiu Eugen Vlad)
+- Enables eslint in codeclimate job. !17392
+- Port Labels Select dropdown to Vue. !17411
+- Add NOT NULL constraint to projects.namespace_id. !17448
+- Ensure foreign keys on clusters applications. !17488
+- Started translation into Turkish, Indonesian and Filipino. !17526
+- Add documentation for displayed K8s Ingress IP address (#44330). !17836
+- Move Ruby endpoints to OPT_OUT.
+- Upgrade Workhorse to version 3.8.0 to support structured logging.
+- Use host URL to build JIRA remote link icon.
+
+
+## 10.5.6 (2018-03-16)
+
+### Security (2 changes)
+
+- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
+- Fix GitLab Auth0 integration signing in the wrong user.
+
+
## 10.5.5 (2018-03-15)
### Fixed (3 changes)
@@ -261,6 +477,14 @@ entry.
- Adds empty state illustration for pending job.
+## 10.4.6 (2018-03-16)
+
+### Security (2 changes)
+
+- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
+- Fix GitLab Auth0 integration signing in the wrong user.
+
+
## 10.4.5 (2018-03-01)
### Security (1 change)
@@ -492,6 +716,15 @@ entry.
- Use a background migration for issues.closed_at.
+## 10.3.9 (2018-03-16)
+
+### Security (3 changes)
+
+- Fixed some SSRF vulnerabilities in services, hooks and integrations. !2337
+- Update nokogiri to 1.8.2. !16807
+- Fix GitLab Auth0 integration signing in the wrong user.
+
+
## 10.3.8 (2018-03-01)
### Security (1 change)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 8f63f4f9a10..36545ad338e 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.91.0
+0.92.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 1aa5e414fd3..21c8c7b46b8 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-6.0.4
+7.1.1
diff --git a/Gemfile b/Gemfile
index dcd5f5ee049..01fc3263330 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,19 @@
+# --- Special code for migrating to Rails 5.0 ---
+def rails5?
+ %w[1 true].include?(ENV["RAILS5"])
+end
+
+gem_versions = {}
+gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
+gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0'
+gem_versions['html-pipeline'] = rails5? ? '~> 2.6.0' : '~> 1.11.0'
+gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10'
+gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
+# --- The end of special code for migrating to Rails 5.0 ---
+
source 'https://rubygems.org'
-gem 'rails', '4.2.10'
+gem 'rails', gem_versions['rails']
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with
@@ -9,7 +22,7 @@ gem 'responders', '~> 2.0'
gem 'sprockets', '~> 3.7.0'
# Default values for AR models
-gem 'default_value_for', '~> 3.0.0'
+gem 'default_value_for', gem_versions['default_value_for']
# Supported DBs
gem 'mysql2', '~> 0.4.10', group: :mysql
@@ -22,10 +35,10 @@ gem 'faraday', '~> 0.12'
# Authentication libraries
gem 'devise', '~> 4.2'
-gem 'doorkeeper', '~> 4.2.0'
-gem 'doorkeeper-openid_connect', '~> 1.2.0'
-gem 'omniauth', '~> 1.4.2'
-gem 'omniauth-auth0', '~> 1.4.1'
+gem 'doorkeeper', '~> 4.3'
+gem 'doorkeeper-openid_connect', '~> 1.3'
+gem 'omniauth', '~> 1.8'
+gem 'omniauth-auth0', '~> 2.0.0'
gem 'omniauth-azure-oauth2', '~> 0.0.9'
gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0'
@@ -34,11 +47,12 @@ gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.5.2'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
-gem 'omniauth-saml', '~> 1.10.0'
+gem 'omniauth-saml', '~> 1.10'
gem 'omniauth-shibboleth', '~> 1.2.0'
-gem 'omniauth-twitter', '~> 1.2.0'
+gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.1'
+gem 'omniauth-jwt', '~> 0.0.2'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6'
@@ -104,16 +118,16 @@ gem 'carrierwave', '~> 1.2'
gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
-gem 'fog-aws', '~> 2.0'
+gem 'fog-aws', '~> 2.0.1'
gem 'fog-core', '~> 1.44'
-gem 'fog-google', '~> 0.5'
+gem 'fog-google', '~> 1.3.3'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.2.0'
# for Google storage
-gem 'google-api-client', '~> 0.13.6'
+gem 'google-api-client', '~> 0.19.8'
# for aws storage
gem 'unf', '~> 0.1.4'
@@ -122,7 +136,7 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing
-gem 'html-pipeline', '~> 1.11.0'
+gem 'html-pipeline', gem_versions['html-pipeline']
gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4'
@@ -132,8 +146,8 @@ gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
-gem 'asciidoctor', '~> 1.5.2'
-gem 'asciidoctor-plantuml', '0.0.7'
+gem 'asciidoctor', '~> 1.5.6'
+gem 'asciidoctor-plantuml', '0.0.8'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
@@ -149,7 +163,7 @@ group :unicorn do
end
# State machine
-gem 'state_machines-activerecord', '~> 0.4.0'
+gem 'state_machines-activerecord', '~> 0.5.1'
# Issue tags
gem 'acts-as-taggable-on', '~> 5.0'
@@ -218,10 +232,10 @@ gem 'sanitize', '~> 2.0'
gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input
-gem 'loofah', '~> 2.0.3'
+gem 'loofah', '~> 2.2'
# Working with license
-gem 'licensee', '~> 8.7.0'
+gem 'licensee', '~> 8.9'
# Protect against bruteforcing
gem 'rack-attack', '~> 4.4.1'
@@ -235,9 +249,6 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.5'
-# Faster JSON
-gem 'oj', '~> 2.17.4'
-
# Faster blank
gem 'fast_blank'
@@ -269,9 +280,9 @@ gem 'premailer-rails', '~> 1.9.7'
# I18n
gem 'ruby_parser', '~> 3.8', require: false
-gem 'rails-i18n', '~> 4.0.9'
+gem 'rails-i18n', gem_versions['rails-i18n']
gem 'gettext_i18n_rails', '~> 1.8.0'
-gem 'gettext_i18n_rails_js', '~> 1.2.0'
+gem 'gettext_i18n_rails_js', '~> 1.3'
gem 'gettext', '~> 3.2.2', require: false, group: :development
gem 'batch-loader', '~> 1.2.1'
@@ -279,7 +290,6 @@ gem 'batch-loader', '~> 1.2.1'
# Perf bar
gem 'peek', '~> 1.0.1'
gem 'peek-gc', '~> 0.0.2'
-gem 'peek-host', '~> 1.0.0'
gem 'peek-mysql2', '~> 1.1.0', group: :mysql
gem 'peek-performance_bar', '~> 1.3.0'
gem 'peek-pg', '~> 1.3.0', group: :postgres
@@ -361,7 +371,7 @@ group :development, :test do
gem 'license_finder', '~> 3.1', require: false
gem 'knapsack', '~> 1.16'
- gem 'activerecord_sane_schema_dumper', '0.2'
+ gem 'activerecord_sane_schema_dumper', gem_versions['activerecord_sane_schema_dumper']
gem 'stackprof', '~> 0.2.10', require: false
@@ -411,7 +421,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
diff --git a/Gemfile.lock b/Gemfile.lock
index 618fbf4b3e7..5b61f3dbf67 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -47,6 +47,7 @@ GEM
memoizable (~> 0.4.0)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
+ aes_key_wrap (1.0.1)
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
@@ -55,8 +56,8 @@ GEM
faraday_middleware (~> 0.9)
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
- asciidoctor (1.5.3)
- asciidoctor-plantuml (0.0.7)
+ asciidoctor (1.5.6.2)
+ asciidoctor-plantuml (0.0.8)
asciidoctor (~> 1.5)
asset_sync (2.2.0)
activemodel (>= 4.1.0)
@@ -86,7 +87,7 @@ GEM
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
- bindata (2.4.1)
+ bindata (2.4.3)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
blankslate (2.1.2.4)
@@ -142,6 +143,7 @@ GEM
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
+ crass (1.0.3)
creole (0.5.0)
css_parser (1.5.0)
addressable
@@ -176,10 +178,10 @@ GEM
docile (1.1.5)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (4.2.6)
+ doorkeeper (4.3.1)
railties (>= 4.2)
- doorkeeper-openid_connect (1.2.0)
- doorkeeper (~> 4.0)
+ doorkeeper-openid_connect (1.3.0)
+ doorkeeper (~> 4.3)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
@@ -210,7 +212,7 @@ GEM
faraday_middleware
multi_json
fast_blank (1.0.0)
- fast_gettext (1.4.0)
+ fast_gettext (1.6.0)
ffaker (2.4.0)
ffi (1.9.18)
flay (2.10.0)
@@ -242,10 +244,11 @@ GEM
builder
excon (~> 0.58)
formatador (~> 0.2)
- fog-google (0.5.3)
+ fog-google (1.3.3)
fog-core
fog-json
fog-xml
+ google-api-client (~> 0.19.1)
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
@@ -276,18 +279,18 @@ GEM
gemojione (3.3.0)
json
get_process_mem (0.2.0)
- gettext (3.2.2)
+ gettext (3.2.9)
locale (>= 2.0.5)
text (>= 1.3.0)
gettext_i18n_rails (1.8.0)
fast_gettext (>= 0.9.0)
- gettext_i18n_rails_js (1.2.0)
+ gettext_i18n_rails_js (1.3.0)
gettext (>= 3.0.2)
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.88.0)
+ gitaly-proto (0.91.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (5.3.3)
@@ -335,9 +338,9 @@ GEM
json
multi_json
request_store (>= 1.0)
- google-api-client (0.13.6)
+ google-api-client (0.19.8)
addressable (~> 2.5, >= 2.5.1)
- googleauth (~> 0.5)
+ googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0)
mime-types (~> 3.0)
representable (~> 3.0)
@@ -355,7 +358,7 @@ GEM
signet (~> 0.7)
gpgme (2.0.13)
mini_portile2 (~> 2.1)
- grape (1.0.0)
+ grape (1.0.2)
activesupport
builder
mustermann-grape (~> 1.0.0)
@@ -414,7 +417,7 @@ GEM
httparty (0.13.7)
json (~> 1.8)
multi_xml (>= 0.5.2)
- httpclient (2.8.2)
+ httpclient (2.8.3)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
@@ -428,10 +431,10 @@ GEM
oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
json (1.8.6)
- json-jwt (1.7.2)
+ json-jwt (1.9.2)
activesupport
+ aes_key_wrap
bindata
- multi_json (>= 1.3)
securecompare
url_safe_base64
json-schema (2.8.0)
@@ -473,7 +476,7 @@ GEM
toml (= 0.1.2)
with_env (> 1.0)
xml-simple
- licensee (8.7.0)
+ licensee (8.9.2)
rugged (~> 0.24)
little-plugger (1.1.4)
locale (2.1.2)
@@ -484,7 +487,8 @@ GEM
actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2)
- loofah (2.0.3)
+ loofah (2.2.2)
+ crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.0)
mini_mime (>= 0.1.1)
@@ -497,14 +501,14 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.0)
- mini_mime (0.1.4)
+ mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
multi_json (1.13.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
- mustermann (1.0.0)
+ mustermann (1.0.2)
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
mysql2 (0.4.10)
@@ -514,7 +518,7 @@ GEM
nokogiri (1.8.2)
mini_portile2 (~> 2.3.0)
numerizer (0.1.1)
- oauth (0.5.1)
+ oauth (0.5.4)
oauth2 (1.4.0)
faraday (>= 0.8, < 0.13)
jwt (~> 1.0)
@@ -523,12 +527,11 @@ GEM
rack (>= 1.2, < 3)
octokit (4.8.0)
sawyer (~> 0.8.0, >= 0.5.3)
- oj (2.17.5)
- omniauth (1.4.3)
- hashie (>= 1.2, < 4)
+ omniauth (1.8.1)
+ hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3)
- omniauth-auth0 (1.4.1)
- omniauth-oauth2 (~> 1.1)
+ omniauth-auth0 (2.0.0)
+ omniauth-oauth2 (~> 1.4)
omniauth-authentiq (0.3.1)
omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.9)
@@ -552,6 +555,9 @@ GEM
multi_json (~> 1.3)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.3.1)
+ omniauth-jwt (0.0.2)
+ jwt
+ omniauth (~> 1.1)
omniauth-kerberos (0.3.0)
omniauth-multipassword
timfel-krb5-auth (~> 0.8)
@@ -570,9 +576,9 @@ GEM
ruby-saml (~> 1.7)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
- omniauth-twitter (1.2.1)
- json (~> 1.3)
+ omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
+ rack
omniauth_crowd (2.2.3)
activesupport
nokogiri (>= 1.4.4)
@@ -593,8 +599,6 @@ GEM
railties (>= 4.0.0)
peek-gc (0.0.2)
peek
- peek-host (1.0.0)
- peek
peek-mysql2 (1.1.0)
atomic (>= 1.0.0)
mysql2
@@ -658,7 +662,7 @@ GEM
httpclient (>= 2.4)
multi_json (>= 1.3.6)
rack (>= 1.1)
- rack-protection (1.5.3)
+ rack-protection (2.0.1)
rack
rack-proxy (0.6.0)
rack
@@ -677,12 +681,12 @@ GEM
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
- rails-dom-testing (1.0.8)
- activesupport (>= 4.2.0.beta, < 5.0)
+ rails-dom-testing (1.0.9)
+ activesupport (>= 4.2.0, < 5.0)
nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1)
- rails-html-sanitizer (1.0.3)
- loofah (~> 2.0)
+ rails-html-sanitizer (1.0.4)
+ loofah (~> 2.2, >= 2.2.2)
rails-i18n (4.0.9)
i18n (~> 0.7)
railties (~> 4.0)
@@ -897,13 +901,13 @@ GEM
sqlite3 (1.3.13)
sshkey (1.9.0)
stackprof (0.2.10)
- state_machines (0.4.0)
- state_machines-activemodel (0.4.0)
- activemodel (>= 4.1, < 5.1)
- state_machines (>= 0.4.0)
- state_machines-activerecord (0.4.0)
- activerecord (>= 4.1, < 5.1)
- state_machines-activemodel (>= 0.3.0)
+ state_machines (0.5.0)
+ state_machines-activemodel (0.5.1)
+ activemodel (>= 4.1, < 6.0)
+ state_machines (>= 0.5.0)
+ state_machines-activerecord (0.5.1)
+ activerecord (>= 4.1, < 6.0)
+ state_machines-activemodel (>= 0.5.0)
stringex (2.7.1)
sys-filesystem (1.1.6)
ffi
@@ -995,8 +999,8 @@ DEPENDENCIES
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.6.0)
- asciidoctor (~> 1.5.2)
- asciidoctor-plantuml (= 0.0.7)
+ asciidoctor (~> 1.5.6)
+ asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
@@ -1030,8 +1034,8 @@ DEPENDENCIES
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
- doorkeeper (~> 4.2.0)
- doorkeeper-openid_connect (~> 1.2.0)
+ doorkeeper (~> 4.3)
+ doorkeeper-openid_connect (~> 1.3)
dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
@@ -1044,9 +1048,9 @@ DEPENDENCIES
flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.13.0)
fog-aliyun (~> 0.2.0)
- fog-aws (~> 2.0)
+ fog-aws (~> 2.0.1)
fog-core (~> 1.44)
- fog-google (~> 0.5)
+ fog-google (~> 1.3.3)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1)
@@ -1057,8 +1061,8 @@ DEPENDENCIES
gemojione (~> 3.3)
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
- gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.88.0)
+ gettext_i18n_rails_js (~> 1.3)
+ gitaly-proto (~> 0.91.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@@ -1067,7 +1071,7 @@ DEPENDENCIES
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
- google-api-client (~> 0.13.6)
+ google-api-client (~> 0.19.8)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
@@ -1093,9 +1097,9 @@ DEPENDENCIES
kubeclient (~> 3.0)
letter_opener_web (~> 1.3.0)
license_finder (~> 3.1)
- licensee (~> 8.7.0)
+ licensee (~> 8.9)
lograge (~> 0.5)
- loofah (~> 2.0.3)
+ loofah (~> 2.2)
mail_room (~> 0.9.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
@@ -1106,9 +1110,8 @@ DEPENDENCIES
nokogiri (~> 1.8.2)
oauth2 (~> 1.4)
octokit (~> 4.8)
- oj (~> 2.17.4)
- omniauth (~> 1.4.2)
- omniauth-auth0 (~> 1.4.1)
+ omniauth (~> 1.8)
+ omniauth-auth0 (~> 2.0.0)
omniauth-authentiq (~> 0.3.1)
omniauth-azure-oauth2 (~> 0.0.9)
omniauth-cas3 (~> 1.1.4)
@@ -1116,16 +1119,16 @@ DEPENDENCIES
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.2)
+ omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
- omniauth-saml (~> 1.10.0)
+ omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.2.0)
- omniauth-twitter (~> 1.2.0)
+ omniauth-twitter (~> 1.4)
omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
peek (~> 1.0.1)
peek-gc (~> 0.0.2)
- peek-host (~> 1.0.0)
peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0)
@@ -1196,7 +1199,7 @@ DEPENDENCIES
sprockets (~> 3.7.0)
sshkey (~> 1.9.0)
stackprof (~> 0.2.10)
- state_machines-activerecord (~> 0.4.0)
+ state_machines-activerecord (~> 0.5.1)
sys-filesystem (~> 1.1.6)
test-prof (~> 0.2.5)
test_after_commit (~> 1.1)
diff --git a/Gemfile.rails5 b/Gemfile.rails5
new file mode 100644
index 00000000000..2b526b19ba0
--- /dev/null
+++ b/Gemfile.rails5
@@ -0,0 +1,7 @@
+# BUNDLE_GEMFILE=Gemfile.rails5 bundle install
+
+ENV["RAILS5"] = "true"
+
+gemfile = File.expand_path("../Gemfile", __FILE__)
+
+eval(File.read(gemfile), nil, gemfile)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
new file mode 100644
index 00000000000..86a22dbe550
--- /dev/null
+++ b/Gemfile.rails5.lock
@@ -0,0 +1,1219 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ RedCloth (4.3.2)
+ abstract_type (0.0.7)
+ ace-rails-ap (4.1.4)
+ actioncable (5.0.6)
+ actionpack (= 5.0.6)
+ nio4r (>= 1.2, < 3.0)
+ websocket-driver (~> 0.6.1)
+ actionmailer (5.0.6)
+ actionpack (= 5.0.6)
+ actionview (= 5.0.6)
+ activejob (= 5.0.6)
+ mail (~> 2.5, >= 2.5.4)
+ rails-dom-testing (~> 2.0)
+ actionpack (5.0.6)
+ actionview (= 5.0.6)
+ activesupport (= 5.0.6)
+ rack (~> 2.0)
+ rack-test (~> 0.6.3)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ actionview (5.0.6)
+ activesupport (= 5.0.6)
+ builder (~> 3.1)
+ erubis (~> 2.7.0)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
+ activejob (5.0.6)
+ activesupport (= 5.0.6)
+ globalid (>= 0.3.6)
+ activemodel (5.0.6)
+ activesupport (= 5.0.6)
+ activerecord (5.0.6)
+ activemodel (= 5.0.6)
+ activesupport (= 5.0.6)
+ arel (~> 7.0)
+ activerecord_sane_schema_dumper (1.0)
+ rails (>= 5, < 6)
+ activesupport (5.0.6)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (~> 0.7)
+ minitest (~> 5.1)
+ tzinfo (~> 1.1)
+ acts-as-taggable-on (5.0.0)
+ activerecord (>= 4.2.8)
+ adamantium (0.2.0)
+ ice_nine (~> 0.11.0)
+ memoizable (~> 0.4.0)
+ addressable (2.5.2)
+ public_suffix (>= 2.0.2, < 4.0)
+ aes_key_wrap (1.0.1)
+ akismet (2.0.0)
+ allocations (1.0.5)
+ arel (7.1.4)
+ asana (0.6.3)
+ faraday (~> 0.9)
+ faraday_middleware (~> 0.9)
+ faraday_middleware-multi_json (~> 0.0)
+ oauth2 (~> 1.0)
+ asciidoctor (1.5.6.1)
+ asciidoctor-plantuml (0.0.7)
+ asciidoctor (~> 1.5)
+ asset_sync (2.2.0)
+ activemodel (>= 4.1.0)
+ fog-core
+ mime-types (>= 2.99)
+ unf
+ ast (2.4.0)
+ atomic (1.1.100)
+ attr_encrypted (3.0.3)
+ encryptor (~> 3.0.0)
+ attr_required (1.0.1)
+ autoprefixer-rails (8.1.0.1)
+ execjs
+ awesome_print (1.2.0)
+ axiom-types (0.1.1)
+ descendants_tracker (~> 0.0.4)
+ ice_nine (~> 0.11.0)
+ thread_safe (~> 0.3, >= 0.3.1)
+ babosa (1.0.2)
+ base32 (0.3.2)
+ batch-loader (1.2.1)
+ bcrypt (3.1.11)
+ bcrypt_pbkdf (1.0.0)
+ benchmark-ips (2.3.0)
+ better_errors (2.1.1)
+ coderay (>= 1.0.0)
+ erubis (>= 2.6.6)
+ rack (>= 0.9.0)
+ bindata (2.4.3)
+ binding_of_caller (0.7.3)
+ debug_inspector (>= 0.0.1)
+ blankslate (2.1.2.4)
+ bootstrap-sass (3.3.7)
+ autoprefixer-rails (>= 5.2.1)
+ sass (>= 3.3.4)
+ bootstrap_form (2.7.0)
+ brakeman (3.6.2)
+ browser (2.5.3)
+ builder (3.2.3)
+ bullet (5.5.1)
+ activesupport (>= 3.0.0)
+ uniform_notifier (~> 1.10.0)
+ bundler-audit (0.5.0)
+ bundler (~> 1.2)
+ thor (~> 0.18)
+ byebug (9.0.6)
+ capybara (2.18.0)
+ addressable
+ mini_mime (>= 0.1.3)
+ nokogiri (>= 1.3.3)
+ rack (>= 1.0.0)
+ rack-test (>= 0.5.4)
+ xpath (>= 2.0, < 4.0)
+ capybara-screenshot (1.0.18)
+ capybara (>= 1.0, < 3)
+ launchy
+ carrierwave (1.2.2)
+ activemodel (>= 4.0.0)
+ activesupport (>= 4.0.0)
+ mime-types (>= 1.16)
+ charlock_holmes (0.7.5)
+ childprocess (0.9.0)
+ ffi (~> 1.0, >= 1.0.11)
+ chronic (0.10.2)
+ chronic_duration (0.10.6)
+ numerizer (~> 0.1.1)
+ chunky_png (1.3.10)
+ citrus (3.0.2)
+ coderay (1.1.2)
+ coercible (1.0.0)
+ descendants_tracker (~> 0.0.1)
+ colorize (0.8.1)
+ commonmarker (0.17.9)
+ ruby-enum (~> 0.5)
+ concord (0.1.5)
+ adamantium (~> 0.2.0)
+ equalizer (~> 0.0.9)
+ concurrent-ruby (1.0.5)
+ concurrent-ruby-ext (1.0.5)
+ concurrent-ruby (= 1.0.5)
+ connection_pool (2.2.1)
+ crack (0.4.3)
+ safe_yaml (~> 1.0.0)
+ creole (0.5.0)
+ css_parser (1.6.0)
+ addressable
+ d3_rails (3.5.17)
+ railties (>= 3.1.0)
+ daemons (1.2.6)
+ database_cleaner (1.5.3)
+ debug_inspector (0.0.3)
+ debugger-ruby_core_source (1.3.8)
+ deckar01-task_list (2.0.0)
+ html-pipeline
+ declarative (0.0.10)
+ declarative-option (0.1.0)
+ default_value_for (3.0.5)
+ activerecord (>= 3.2.0, < 5.2)
+ descendants_tracker (0.0.4)
+ thread_safe (~> 0.3, >= 0.3.1)
+ devise (4.4.1)
+ bcrypt (~> 3.0)
+ orm_adapter (~> 0.1)
+ railties (>= 4.1.0, < 5.2)
+ responders
+ warden (~> 1.2.3)
+ devise-two-factor (3.0.2)
+ activesupport (< 5.2)
+ attr_encrypted (>= 1.3, < 4, != 2)
+ devise (~> 4.0)
+ railties (< 5.2)
+ rotp (~> 2.0)
+ diff-lcs (1.3)
+ diffy (3.1.0)
+ docile (1.1.5)
+ domain_name (0.5.20170404)
+ unf (>= 0.0.5, < 1.0.0)
+ doorkeeper (4.3.1)
+ railties (>= 4.2)
+ doorkeeper-openid_connect (1.3.0)
+ doorkeeper (~> 4.3)
+ json-jwt (~> 1.6)
+ dropzonejs-rails (0.7.4)
+ rails (> 3.1)
+ email_reply_trimmer (0.1.10)
+ email_spec (1.6.0)
+ launchy (~> 2.1)
+ mail (~> 2.2)
+ encryptor (3.0.0)
+ equalizer (0.0.11)
+ erubis (2.7.0)
+ escape_utils (1.1.1)
+ et-orbi (1.0.9)
+ tzinfo
+ eventmachine (1.2.5)
+ excon (0.60.0)
+ execjs (2.7.0)
+ expression_parser (0.9.0)
+ factory_bot (4.8.2)
+ activesupport (>= 3.0.0)
+ factory_bot_rails (4.8.2)
+ factory_bot (~> 4.8.2)
+ railties (>= 3.0.0)
+ faraday (0.12.2)
+ multipart-post (>= 1.2, < 3)
+ faraday_middleware (0.12.2)
+ faraday (>= 0.7.4, < 1.0)
+ faraday_middleware-multi_json (0.0.6)
+ faraday_middleware
+ multi_json
+ fast_blank (1.0.0)
+ fast_gettext (1.6.0)
+ ffaker (2.8.1)
+ ffi (1.9.23)
+ flay (2.10.0)
+ erubis (~> 2.7.0)
+ path_expander (~> 1.0)
+ ruby_parser (~> 3.0)
+ sexp_processor (~> 4.0)
+ flipper (0.13.0)
+ flipper-active_record (0.13.0)
+ activerecord (>= 3.2, < 6)
+ flipper (~> 0.13.0)
+ flipper-active_support_cache_store (0.13.0)
+ activesupport (>= 3.2, < 6)
+ flipper (~> 0.13.0)
+ flowdock (0.7.1)
+ httparty (~> 0.7)
+ multi_json
+ fog-aliyun (0.2.0)
+ fog-core (~> 1.27)
+ fog-json (~> 1.0)
+ ipaddress (~> 0.8)
+ xml-simple (~> 1.1)
+ fog-aws (2.0.1)
+ fog-core (~> 1.38)
+ fog-json (~> 1.0)
+ fog-xml (~> 0.1)
+ ipaddress (~> 0.8)
+ fog-core (1.45.0)
+ builder
+ excon (~> 0.58)
+ formatador (~> 0.2)
+ fog-google (0.6.0)
+ fog-core
+ fog-json
+ fog-xml
+ fog-json (1.0.2)
+ fog-core (~> 1.0)
+ multi_json (~> 1.10)
+ fog-local (0.5.0)
+ fog-core (>= 1.27, < 3.0)
+ fog-openstack (0.1.24)
+ fog-core (~> 1.40)
+ fog-json (>= 1.0)
+ ipaddress (>= 0.8)
+ fog-rackspace (0.1.5)
+ fog-core (>= 1.35)
+ fog-json (>= 1.0)
+ fog-xml (>= 0.1)
+ ipaddress (>= 0.8)
+ fog-xml (0.1.3)
+ fog-core
+ nokogiri (>= 1.5.11, < 2.0.0)
+ font-awesome-rails (4.7.0.3)
+ railties (>= 3.2, < 5.2)
+ foreman (0.84.0)
+ thor (~> 0.19.1)
+ formatador (0.2.5)
+ fuubar (2.2.0)
+ rspec-core (~> 3.0)
+ ruby-progressbar (~> 1.4)
+ gemnasium-gitlab-service (0.2.6)
+ rugged (~> 0.21)
+ gemojione (3.3.0)
+ json
+ get_process_mem (0.2.1)
+ gettext (3.2.9)
+ locale (>= 2.0.5)
+ text (>= 1.3.0)
+ gettext_i18n_rails (1.8.0)
+ fast_gettext (>= 0.9.0)
+ gettext_i18n_rails_js (1.3.0)
+ gettext (>= 3.0.2)
+ gettext_i18n_rails (>= 0.7.1)
+ po_to_json (>= 1.0.0)
+ rails (>= 3.2.0)
+ gherkin-ruby (0.3.2)
+ gitaly-proto (0.88.0)
+ google-protobuf (~> 3.1)
+ grpc (~> 1.0)
+ github-linguist (5.3.3)
+ charlock_holmes (~> 0.7.5)
+ escape_utils (~> 1.1.0)
+ mime-types (>= 1.19)
+ rugged (>= 0.25.1)
+ github-markup (1.7.0)
+ gitlab-flowdock-git-hook (1.0.1)
+ flowdock (~> 0.7)
+ gitlab-grit (>= 2.4.1)
+ multi_json
+ gitlab-grit (2.8.2)
+ charlock_holmes (~> 0.6)
+ diff-lcs (~> 1.1)
+ mime-types (>= 1.16)
+ posix-spawn (~> 0.3)
+ gitlab-markup (1.6.3)
+ gitlab-styles (2.3.2)
+ rubocop (~> 0.51)
+ rubocop-gitlab-security (~> 0.1.0)
+ rubocop-rspec (~> 1.19)
+ gitlab_omniauth-ldap (2.0.4)
+ net-ldap (~> 0.16)
+ omniauth (~> 1.3)
+ pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
+ rubyntlm (~> 0.5)
+ globalid (0.4.1)
+ activesupport (>= 4.2.0)
+ gollum-grit_adapter (1.0.1)
+ gitlab-grit (~> 2.7, >= 2.7.1)
+ gollum-lib (4.2.7)
+ gemojione (~> 3.2)
+ github-markup (~> 1.6)
+ gollum-grit_adapter (~> 1.0)
+ nokogiri (>= 1.6.1, < 2.0)
+ rouge (~> 2.1)
+ sanitize (~> 2.1)
+ stringex (~> 2.6)
+ gollum-rugged_adapter (0.4.4)
+ mime-types (>= 1.15)
+ rugged (~> 0.25)
+ gon (6.1.0)
+ actionpack (>= 3.0)
+ json
+ multi_json
+ request_store (>= 1.0)
+ google-api-client (0.19.8)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (>= 0.5, < 0.7.0)
+ httpclient (>= 2.8.1, < 3.0)
+ mime-types (~> 3.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.0)
+ google-protobuf (3.5.1)
+ googleapis-common-protos-types (1.0.1)
+ google-protobuf (~> 3.0)
+ googleauth (0.6.2)
+ faraday (~> 0.12)
+ jwt (>= 1.4, < 3.0)
+ logging (~> 2.0)
+ memoist (~> 0.12)
+ multi_json (~> 1.11)
+ os (~> 0.9)
+ signet (~> 0.7)
+ gpgme (2.0.16)
+ mini_portile2 (~> 2.3)
+ grape (1.0.2)
+ activesupport
+ builder
+ mustermann-grape (~> 1.0.0)
+ rack (>= 1.3.0)
+ rack-accept
+ virtus (>= 1.0.0)
+ grape-entity (0.6.1)
+ activesupport (>= 5.0.0)
+ multi_json (>= 1.3.2)
+ grape-route-helpers (2.1.0)
+ activesupport
+ grape (>= 0.16.0)
+ rake
+ grape_logging (1.7.0)
+ grape
+ grpc (1.10.0)
+ google-protobuf (~> 3.1)
+ googleapis-common-protos-types (~> 1.0.0)
+ googleauth (>= 0.5.1, < 0.7)
+ haml (4.0.7)
+ tilt
+ haml_lint (0.26.0)
+ haml (>= 4.0, < 5.1)
+ rainbow
+ rake (>= 10, < 13)
+ rubocop (>= 0.49.0)
+ sysexits (~> 1.1)
+ hamlit (2.6.2)
+ temple (~> 0.7.6)
+ thor
+ tilt
+ hashdiff (0.3.7)
+ hashie (3.5.7)
+ hashie-forbidden_attributes (0.1.1)
+ hashie (>= 3.0)
+ health_check (2.6.0)
+ rails (>= 4.0)
+ hipchat (1.5.4)
+ httparty
+ mimemagic
+ html-pipeline (2.6.0)
+ activesupport (>= 2)
+ nokogiri (>= 1.4)
+ html2text (0.2.1)
+ nokogiri (~> 1.6)
+ htmlentities (4.3.4)
+ http (2.2.2)
+ addressable (~> 2.3)
+ http-cookie (~> 1.0)
+ http-form_data (~> 1.0.1)
+ http_parser.rb (~> 0.6.0)
+ http-cookie (1.0.3)
+ domain_name (~> 0.5)
+ http-form_data (1.0.3)
+ http_parser.rb (0.6.0)
+ httparty (0.13.7)
+ json (~> 1.8)
+ multi_xml (>= 0.5.2)
+ httpclient (2.8.3)
+ i18n (0.9.5)
+ concurrent-ruby (~> 1.0)
+ ice_nine (0.11.2)
+ influxdb (0.5.3)
+ ipaddress (0.8.3)
+ jira-ruby (1.5.0)
+ activesupport
+ multipart-post
+ oauth (~> 0.5, >= 0.5.0)
+ jquery-atwho-rails (1.3.2)
+ json (1.8.6)
+ json-jwt (1.9.2)
+ activesupport
+ aes_key_wrap
+ bindata
+ securecompare
+ url_safe_base64
+ json-schema (2.8.0)
+ addressable (>= 2.4)
+ jwt (1.5.6)
+ kaminari (1.1.1)
+ activesupport (>= 4.1.0)
+ kaminari-actionview (= 1.1.1)
+ kaminari-activerecord (= 1.1.1)
+ kaminari-core (= 1.1.1)
+ kaminari-actionview (1.1.1)
+ actionview
+ kaminari-core (= 1.1.1)
+ kaminari-activerecord (1.1.1)
+ activerecord
+ kaminari-core (= 1.1.1)
+ kaminari-core (1.1.1)
+ kgio (2.11.2)
+ knapsack (1.16.0)
+ rake
+ kubeclient (3.0.0)
+ http (~> 2.2.2)
+ recursive-open-struct (~> 1.0.4)
+ rest-client (~> 2.0)
+ launchy (2.4.3)
+ addressable (~> 2.3)
+ letter_opener (1.6.0)
+ launchy (~> 2.2)
+ letter_opener_web (1.3.3)
+ actionmailer (>= 3.2)
+ letter_opener (~> 1.0)
+ railties (>= 3.2)
+ license_finder (3.1.1)
+ bundler
+ httparty
+ rubyzip
+ thor
+ toml (= 0.1.2)
+ with_env (> 1.0)
+ xml-simple
+ licensee (8.9.2)
+ rugged (~> 0.24)
+ little-plugger (1.1.4)
+ locale (2.1.2)
+ logging (2.2.2)
+ little-plugger (~> 1.1)
+ multi_json (~> 1.10)
+ lograge (0.9.0)
+ actionpack (>= 4)
+ activesupport (>= 4)
+ railties (>= 4)
+ request_store (~> 1.0)
+ loofah (2.0.3)
+ nokogiri (>= 1.5.9)
+ mail (2.7.0)
+ mini_mime (>= 0.1.1)
+ mail_room (0.9.1)
+ memoist (0.16.0)
+ memoizable (0.4.2)
+ thread_safe (~> 0.3, >= 0.3.1)
+ method_source (0.9.0)
+ mime-types (3.1)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2016.0521)
+ mimemagic (0.3.2)
+ mini_mime (1.0.0)
+ mini_portile2 (2.3.0)
+ minitest (5.7.0)
+ mousetrap-rails (1.4.6)
+ multi_json (1.13.1)
+ multi_xml (0.6.0)
+ multipart-post (2.0.0)
+ mustermann (1.0.2)
+ mustermann-grape (1.0.0)
+ mustermann (~> 1.0.0)
+ mysql2 (0.4.10)
+ net-ldap (0.16.1)
+ net-ssh (4.2.0)
+ netrc (0.11.0)
+ nio4r (2.2.0)
+ nokogiri (1.8.2)
+ mini_portile2 (~> 2.3.0)
+ numerizer (0.1.1)
+ oauth (0.5.4)
+ oauth2 (1.4.0)
+ faraday (>= 0.8, < 0.13)
+ jwt (~> 1.0)
+ multi_json (~> 1.3)
+ multi_xml (~> 0.5)
+ rack (>= 1.2, < 3)
+ octokit (4.8.0)
+ sawyer (~> 0.8.0, >= 0.5.3)
+ omniauth (1.8.1)
+ hashie (>= 3.4.6, < 3.6.0)
+ rack (>= 1.6.2, < 3)
+ omniauth-auth0 (1.4.2)
+ omniauth-oauth2 (~> 1.1)
+ omniauth-authentiq (0.3.1)
+ omniauth-oauth2 (~> 1.3, >= 1.3.1)
+ omniauth-azure-oauth2 (0.0.9)
+ jwt (~> 1.0)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.4)
+ omniauth-cas3 (1.1.4)
+ addressable (~> 2.3)
+ nokogiri (~> 1.7, >= 1.7.1)
+ omniauth (~> 1.2)
+ omniauth-facebook (4.0.0)
+ omniauth-oauth2 (~> 1.2)
+ omniauth-github (1.1.2)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.1)
+ omniauth-gitlab (1.0.3)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.0)
+ omniauth-google-oauth2 (0.5.3)
+ jwt (>= 1.5)
+ omniauth (>= 1.1.1)
+ omniauth-oauth2 (>= 1.5)
+ omniauth-kerberos (0.3.0)
+ omniauth-multipassword
+ timfel-krb5-auth (~> 0.8)
+ omniauth-multipassword (0.4.2)
+ omniauth (~> 1.0)
+ omniauth-oauth (1.1.0)
+ oauth
+ omniauth (~> 1.0)
+ omniauth-oauth2 (1.5.0)
+ oauth2 (~> 1.1)
+ omniauth (~> 1.2)
+ omniauth-oauth2-generic (0.2.4)
+ omniauth-oauth2 (~> 1.0)
+ omniauth-saml (1.10.0)
+ omniauth (~> 1.3, >= 1.3.2)
+ ruby-saml (~> 1.7)
+ omniauth-shibboleth (1.2.1)
+ omniauth (>= 1.0.0)
+ omniauth-twitter (1.2.1)
+ json (~> 1.3)
+ omniauth-oauth (~> 1.1)
+ omniauth_crowd (2.2.3)
+ activesupport
+ nokogiri (>= 1.4.4)
+ omniauth (~> 1.0)
+ org-ruby (0.9.12)
+ rubypants (~> 0.2)
+ orm_adapter (0.5.0)
+ os (0.9.6)
+ parallel (1.12.1)
+ parser (2.5.0.4)
+ ast (~> 2.4.0)
+ parslet (1.5.0)
+ blankslate (~> 2.0)
+ path_expander (1.0.2)
+ peek (1.0.1)
+ concurrent-ruby (>= 0.9.0)
+ concurrent-ruby-ext (>= 0.9.0)
+ railties (>= 4.0.0)
+ peek-gc (0.0.2)
+ peek
+ peek-mysql2 (1.1.0)
+ atomic (>= 1.0.0)
+ mysql2
+ peek
+ peek-performance_bar (1.3.1)
+ peek (>= 0.1.0)
+ peek-pg (1.3.0)
+ concurrent-ruby
+ concurrent-ruby-ext
+ peek
+ pg
+ peek-rblineprof (0.2.0)
+ peek
+ rblineprof
+ peek-redis (1.2.0)
+ atomic (>= 1.0.0)
+ peek
+ redis
+ peek-sidekiq (1.0.3)
+ atomic (>= 1.0.0)
+ peek
+ sidekiq
+ pg (0.18.4)
+ po_to_json (1.0.1)
+ json (>= 1.6.0)
+ posix-spawn (0.3.13)
+ powerpack (0.1.1)
+ premailer (1.11.1)
+ addressable
+ css_parser (>= 1.6.0)
+ htmlentities (>= 4.0.0)
+ premailer-rails (1.9.7)
+ actionmailer (>= 3, < 6)
+ premailer (~> 1.7, >= 1.7.9)
+ proc_to_ast (0.1.0)
+ coderay
+ parser
+ unparser
+ procto (0.0.3)
+ prometheus-client-mmap (0.9.1)
+ pry (0.11.3)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ pry-byebug (3.4.3)
+ byebug (>= 9.0, < 9.1)
+ pry (~> 0.10)
+ pry-rails (0.3.6)
+ pry (>= 0.10.4)
+ public_suffix (3.0.2)
+ pyu-ruby-sasl (0.0.3.3)
+ rack (2.0.4)
+ rack-accept (0.4.5)
+ rack (>= 0.4)
+ rack-attack (4.4.1)
+ rack
+ rack-cors (1.0.2)
+ rack-oauth2 (1.2.3)
+ activesupport (>= 2.3)
+ attr_required (>= 0.0.5)
+ httpclient (>= 2.4)
+ multi_json (>= 1.3.6)
+ rack (>= 1.1)
+ rack-protection (2.0.1)
+ rack
+ rack-proxy (0.6.4)
+ rack
+ rack-test (0.6.3)
+ rack (>= 1.0)
+ rails (5.0.6)
+ actioncable (= 5.0.6)
+ actionmailer (= 5.0.6)
+ actionpack (= 5.0.6)
+ actionview (= 5.0.6)
+ activejob (= 5.0.6)
+ activemodel (= 5.0.6)
+ activerecord (= 5.0.6)
+ activesupport (= 5.0.6)
+ bundler (>= 1.3.0)
+ railties (= 5.0.6)
+ sprockets-rails (>= 2.0.0)
+ rails-deprecated_sanitizer (1.0.3)
+ activesupport (>= 4.2.0.alpha)
+ rails-dom-testing (2.0.3)
+ activesupport (>= 4.2.0)
+ nokogiri (>= 1.6)
+ rails-html-sanitizer (1.0.3)
+ loofah (~> 2.0)
+ rails-i18n (5.1.1)
+ i18n (>= 0.7, < 2)
+ railties (>= 5.0, < 6)
+ railties (5.0.6)
+ actionpack (= 5.0.6)
+ activesupport (= 5.0.6)
+ method_source
+ rake (>= 0.8.7)
+ thor (>= 0.18.1, < 2.0)
+ rainbow (2.2.2)
+ rake
+ raindrops (0.19.0)
+ rake (12.3.0)
+ rb-fsevent (0.10.3)
+ rb-inotify (0.9.10)
+ ffi (>= 0.5.0, < 2)
+ rblineprof (0.3.7)
+ debugger-ruby_core_source (~> 1.3)
+ rbnacl (4.0.2)
+ ffi
+ rbnacl-libsodium (1.0.16)
+ rbnacl (>= 3.0.1)
+ rdoc (4.3.0)
+ re2 (1.1.1)
+ recaptcha (3.4.0)
+ json
+ recursive-open-struct (1.0.5)
+ redcarpet (3.4.0)
+ redis (3.3.5)
+ redis-actionpack (5.0.2)
+ actionpack (>= 4.0, < 6)
+ redis-rack (>= 1, < 3)
+ redis-store (>= 1.1.0, < 2)
+ redis-activesupport (5.0.4)
+ activesupport (>= 3, < 6)
+ redis-store (>= 1.3, < 2)
+ redis-namespace (1.5.3)
+ redis (~> 3.0, >= 3.0.4)
+ redis-rack (2.0.4)
+ rack (>= 1.5, < 3)
+ redis-store (>= 1.2, < 2)
+ redis-rails (5.0.2)
+ redis-actionpack (>= 5.0, < 6)
+ redis-activesupport (>= 5.0, < 6)
+ redis-store (>= 1.2, < 2)
+ redis-store (1.4.1)
+ redis (>= 2.2, < 5)
+ representable (3.0.4)
+ declarative (< 0.1.0)
+ declarative-option (< 0.2.0)
+ uber (< 0.2.0)
+ request_store (1.4.0)
+ rack (>= 1.4)
+ responders (2.4.0)
+ actionpack (>= 4.2.0, < 5.3)
+ railties (>= 4.2.0, < 5.3)
+ rest-client (2.0.2)
+ http-cookie (>= 1.0.2, < 2.0)
+ mime-types (>= 1.16, < 4.0)
+ netrc (~> 0.8)
+ retriable (3.1.1)
+ rinku (2.0.4)
+ rotp (2.1.2)
+ rouge (2.2.1)
+ rqrcode (0.10.1)
+ chunky_png (~> 1.0)
+ rqrcode-rails3 (0.1.7)
+ rqrcode (>= 0.4.2)
+ rspec (3.6.0)
+ rspec-core (~> 3.6.0)
+ rspec-expectations (~> 3.6.0)
+ rspec-mocks (~> 3.6.0)
+ rspec-core (3.6.0)
+ rspec-support (~> 3.6.0)
+ rspec-expectations (3.6.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.6.0)
+ rspec-mocks (3.6.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.6.0)
+ rspec-parameterized (0.4.0)
+ binding_of_caller
+ parser
+ proc_to_ast
+ rspec (>= 2.13, < 4)
+ unparser
+ rspec-rails (3.6.1)
+ actionpack (>= 3.0)
+ activesupport (>= 3.0)
+ railties (>= 3.0)
+ rspec-core (~> 3.6.0)
+ rspec-expectations (~> 3.6.0)
+ rspec-mocks (~> 3.6.0)
+ rspec-support (~> 3.6.0)
+ rspec-retry (0.4.6)
+ rspec-core
+ rspec-set (0.1.3)
+ rspec-support (3.6.0)
+ rspec_profiling (0.0.5)
+ activerecord
+ pg
+ rails
+ sqlite3
+ rubocop (0.52.1)
+ parallel (~> 1.10)
+ parser (>= 2.4.0.2, < 3.0)
+ powerpack (~> 0.1)
+ rainbow (>= 2.2.2, < 4.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (~> 1.0, >= 1.0.1)
+ rubocop-gitlab-security (0.1.1)
+ rubocop (>= 0.51)
+ rubocop-rspec (1.22.2)
+ rubocop (>= 0.52.1)
+ ruby-enum (0.7.2)
+ i18n
+ ruby-fogbugz (0.2.1)
+ crack (~> 0.4)
+ ruby-prof (0.17.0)
+ ruby-progressbar (1.9.0)
+ ruby-saml (1.7.2)
+ nokogiri (>= 1.5.10)
+ ruby_parser (3.11.0)
+ sexp_processor (~> 4.9)
+ rubyntlm (0.6.2)
+ rubypants (0.7.0)
+ rubyzip (1.2.1)
+ rufus-scheduler (3.4.2)
+ et-orbi (~> 1.0)
+ rugged (0.26.0)
+ safe_yaml (1.0.4)
+ sanitize (2.1.0)
+ nokogiri (>= 1.4.4)
+ sass (3.5.5)
+ sass-listen (~> 4.0.0)
+ sass-listen (4.0.0)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ sass-rails (5.0.7)
+ railties (>= 4.0.0, < 6)
+ sass (~> 3.1)
+ sprockets (>= 2.8, < 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
+ tilt (>= 1.1, < 3)
+ sawyer (0.8.1)
+ addressable (>= 2.3.5, < 2.6)
+ faraday (~> 0.8, < 1.0)
+ scss_lint (0.56.0)
+ rake (>= 0.9, < 13)
+ sass (~> 3.5.3)
+ securecompare (1.0.0)
+ seed-fu (2.3.7)
+ activerecord (>= 3.1)
+ activesupport (>= 3.1)
+ select2-rails (3.5.10)
+ thor (~> 0.14)
+ selenium-webdriver (3.11.0)
+ childprocess (~> 0.5)
+ rubyzip (~> 1.2)
+ sentry-raven (2.7.2)
+ faraday (>= 0.7.6, < 1.0)
+ settingslogic (2.0.9)
+ sexp_processor (4.10.1)
+ sham_rack (1.3.6)
+ rack
+ shoulda-matchers (3.1.2)
+ activesupport (>= 4.0.0)
+ sidekiq (5.1.1)
+ concurrent-ruby (~> 1.0)
+ connection_pool (~> 2.2, >= 2.2.0)
+ rack-protection (>= 1.5.0)
+ redis (>= 3.3.5, < 5)
+ sidekiq-cron (0.6.3)
+ rufus-scheduler (>= 3.3.0)
+ sidekiq (>= 4.2.1)
+ sidekiq-limit_fetch (3.4.0)
+ sidekiq (>= 4)
+ signet (0.8.1)
+ addressable (~> 2.3)
+ faraday (~> 0.9)
+ jwt (>= 1.5, < 3.0)
+ multi_json (~> 1.10)
+ simple_po_parser (1.1.3)
+ simplecov (0.14.1)
+ docile (~> 1.1.0)
+ json (>= 1.8, < 3)
+ simplecov-html (~> 0.10.0)
+ simplecov-html (0.10.2)
+ slack-notifier (1.5.1)
+ spinach (0.10.1)
+ colorize
+ gherkin-ruby (>= 0.3.2)
+ json
+ spinach-rails (0.2.1)
+ capybara (>= 2.0.0)
+ railties (>= 3)
+ spinach (>= 0.4)
+ spinach-rerun-reporter (0.0.2)
+ spinach (~> 0.8)
+ spring (2.0.2)
+ activesupport (>= 4.2)
+ spring-commands-rspec (1.0.4)
+ spring (>= 0.9.1)
+ spring-commands-spinach (1.1.0)
+ spring (>= 0.9.1)
+ sprockets (3.7.1)
+ concurrent-ruby (~> 1.0)
+ rack (> 1, < 3)
+ sprockets-rails (3.2.1)
+ actionpack (>= 4.0)
+ activesupport (>= 4.0)
+ sprockets (>= 3.0.0)
+ sqlite3 (1.3.13)
+ sshkey (1.9.0)
+ stackprof (0.2.11)
+ state_machines (0.5.0)
+ state_machines-activemodel (0.5.1)
+ activemodel (>= 4.1, < 6.0)
+ state_machines (>= 0.5.0)
+ state_machines-activerecord (0.5.1)
+ activerecord (>= 4.1, < 6.0)
+ state_machines-activemodel (>= 0.5.0)
+ stringex (2.8.4)
+ sys-filesystem (1.1.9)
+ ffi
+ sysexits (1.2.0)
+ temple (0.7.7)
+ test-prof (0.2.5)
+ test_after_commit (1.1.0)
+ activerecord (>= 3.2)
+ text (1.3.1)
+ thin (1.7.2)
+ daemons (~> 1.0, >= 1.0.9)
+ eventmachine (~> 1.0, >= 1.0.4)
+ rack (>= 1, < 3)
+ thor (0.19.4)
+ thread_safe (0.3.6)
+ tilt (2.0.8)
+ timecop (0.8.1)
+ timfel-krb5-auth (0.8.3)
+ toml (0.1.2)
+ parslet (~> 1.5.0)
+ toml-rb (1.0.0)
+ citrus (~> 3.0, > 3.0)
+ truncato (0.7.10)
+ htmlentities (~> 4.3.1)
+ nokogiri (~> 1.8.0, >= 1.7.0)
+ tzinfo (1.2.5)
+ thread_safe (~> 0.1)
+ u2f (0.2.1)
+ uber (0.1.0)
+ uglifier (2.7.2)
+ execjs (>= 0.3.0)
+ json (>= 1.8.0)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.7.5)
+ unicode-display_width (1.3.0)
+ unicorn (5.1.0)
+ kgio (~> 2.6)
+ raindrops (~> 0.7)
+ unicorn-worker-killer (0.4.4)
+ get_process_mem (~> 0)
+ unicorn (>= 4, < 6)
+ uniform_notifier (1.10.0)
+ unparser (0.2.7)
+ abstract_type (~> 0.0.7)
+ adamantium (~> 0.2.0)
+ concord (~> 0.1.5)
+ diff-lcs (~> 1.3)
+ equalizer (~> 0.0.9)
+ parser (>= 2.3.1.2, < 2.6)
+ procto (~> 0.0.2)
+ url_safe_base64 (0.2.2)
+ validates_hostname (1.0.8)
+ activerecord (>= 3.0)
+ activesupport (>= 3.0)
+ version_sorter (2.1.0)
+ virtus (1.0.5)
+ axiom-types (~> 0.1)
+ coercible (~> 1.0)
+ descendants_tracker (~> 0.0, >= 0.0.3)
+ equalizer (~> 0.0, >= 0.0.9)
+ vmstat (2.3.0)
+ warden (1.2.7)
+ rack (>= 1.0)
+ webmock (2.3.2)
+ addressable (>= 2.3.6)
+ crack (>= 0.3.2)
+ hashdiff
+ webpack-rails (0.9.11)
+ railties (>= 3.2.0)
+ websocket-driver (0.6.5)
+ websocket-extensions (>= 0.1.0)
+ websocket-extensions (0.1.3)
+ wikicloth (0.8.1)
+ builder
+ expression_parser
+ rinku
+ with_env (1.1.0)
+ xml-simple (1.1.5)
+ xpath (3.0.0)
+ nokogiri (~> 1.8)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ RedCloth (~> 4.3.2)
+ ace-rails-ap (~> 4.1.0)
+ activerecord_sane_schema_dumper (= 1.0)
+ acts-as-taggable-on (~> 5.0)
+ addressable (~> 2.5.2)
+ akismet (~> 2.0)
+ allocations (~> 1.0)
+ asana (~> 0.6.0)
+ asciidoctor (~> 1.5.2)
+ asciidoctor-plantuml (= 0.0.7)
+ asset_sync (~> 2.2.0)
+ attr_encrypted (~> 3.0.0)
+ awesome_print (~> 1.2.0)
+ babosa (~> 1.0.2)
+ base32 (~> 0.3.0)
+ batch-loader (~> 1.2.1)
+ bcrypt_pbkdf (~> 1.0)
+ benchmark-ips (~> 2.3.0)
+ better_errors (~> 2.1.0)
+ binding_of_caller (~> 0.7.2)
+ bootstrap-sass (~> 3.3.0)
+ bootstrap_form (~> 2.7.0)
+ brakeman (~> 3.6.0)
+ browser (~> 2.2)
+ bullet (~> 5.5.0)
+ bundler-audit (~> 0.5.0)
+ capybara (~> 2.15)
+ capybara-screenshot (~> 1.0.0)
+ carrierwave (~> 1.2)
+ charlock_holmes (~> 0.7.5)
+ chronic (~> 0.10.2)
+ chronic_duration (~> 0.10.6)
+ commonmarker (~> 0.17)
+ concurrent-ruby (~> 1.0.5)
+ connection_pool (~> 2.0)
+ creole (~> 0.5.0)
+ d3_rails (~> 3.5.0)
+ database_cleaner (~> 1.5.0)
+ deckar01-task_list (= 2.0.0)
+ default_value_for (~> 3.0.5)
+ devise (~> 4.2)
+ devise-two-factor (~> 3.0.0)
+ diffy (~> 3.1.0)
+ doorkeeper (~> 4.3)
+ doorkeeper-openid_connect (~> 1.3)
+ dropzonejs-rails (~> 0.7.1)
+ email_reply_trimmer (~> 0.1)
+ email_spec (~> 1.6.0)
+ factory_bot_rails (~> 4.8.2)
+ faraday (~> 0.12)
+ fast_blank
+ ffaker (~> 2.4)
+ flay (~> 2.10.0)
+ flipper (~> 0.13.0)
+ flipper-active_record (~> 0.13.0)
+ flipper-active_support_cache_store (~> 0.13.0)
+ fog-aliyun (~> 0.2.0)
+ fog-aws (~> 2.0)
+ fog-core (~> 1.44)
+ fog-google (~> 0.5)
+ fog-local (~> 0.3)
+ fog-openstack (~> 0.1)
+ fog-rackspace (~> 0.1.1)
+ font-awesome-rails (~> 4.7)
+ foreman (~> 0.84.0)
+ fuubar (~> 2.2.0)
+ gemnasium-gitlab-service (~> 0.2)
+ gemojione (~> 3.3)
+ gettext (~> 3.2.2)
+ gettext_i18n_rails (~> 1.8.0)
+ gettext_i18n_rails_js (~> 1.3)
+ gitaly-proto (~> 0.88.0)
+ github-linguist (~> 5.3.3)
+ gitlab-flowdock-git-hook (~> 1.0.1)
+ gitlab-markup (~> 1.6.2)
+ gitlab-styles (~> 2.3)
+ gitlab_omniauth-ldap (~> 2.0.4)
+ gollum-lib (~> 4.2)
+ gollum-rugged_adapter (~> 0.4.4)
+ gon (~> 6.1.0)
+ google-api-client (~> 0.19.8)
+ google-protobuf (= 3.5.1)
+ gpgme
+ grape (~> 1.0)
+ grape-entity (~> 0.6.0)
+ grape-route-helpers (~> 2.1.0)
+ grape_logging (~> 1.7)
+ grpc (~> 1.10.0)
+ haml_lint (~> 0.26.0)
+ hamlit (~> 2.6.1)
+ hashie-forbidden_attributes
+ health_check (~> 2.6.0)
+ hipchat (~> 1.5.0)
+ html-pipeline (~> 2.6.0)
+ html2text
+ httparty (~> 0.13.3)
+ influxdb (~> 0.2)
+ jira-ruby (~> 1.4)
+ jquery-atwho-rails (~> 1.3.2)
+ json-schema (~> 2.8.0)
+ jwt (~> 1.5.6)
+ kaminari (~> 1.0)
+ knapsack (~> 1.16)
+ kubeclient (~> 3.0)
+ letter_opener_web (~> 1.3.0)
+ license_finder (~> 3.1)
+ licensee (~> 8.9)
+ lograge (~> 0.5)
+ loofah (~> 2.0.3)
+ mail_room (~> 0.9.1)
+ method_source (~> 0.8)
+ minitest (~> 5.7.0)
+ mousetrap-rails (~> 1.4.6)
+ mysql2 (~> 0.4.10)
+ net-ldap
+ net-ssh (~> 4.2.0)
+ nokogiri (~> 1.8.2)
+ oauth2 (~> 1.4)
+ octokit (~> 4.8)
+ omniauth (~> 1.8)
+ omniauth-auth0 (~> 1.4.1)
+ omniauth-authentiq (~> 0.3.1)
+ omniauth-azure-oauth2 (~> 0.0.9)
+ omniauth-cas3 (~> 1.1.4)
+ omniauth-facebook (~> 4.0.0)
+ omniauth-github (~> 1.1.1)
+ omniauth-gitlab (~> 1.0.2)
+ omniauth-google-oauth2 (~> 0.5.2)
+ omniauth-kerberos (~> 0.3.0)
+ omniauth-oauth2-generic (~> 0.2.2)
+ omniauth-saml (~> 1.10)
+ omniauth-shibboleth (~> 1.2.0)
+ omniauth-twitter (~> 1.2.0)
+ omniauth_crowd (~> 2.2.0)
+ org-ruby (~> 0.9.12)
+ peek (~> 1.0.1)
+ peek-gc (~> 0.0.2)
+ peek-mysql2 (~> 1.1.0)
+ peek-performance_bar (~> 1.3.0)
+ peek-pg (~> 1.3.0)
+ peek-rblineprof (~> 0.2.0)
+ peek-redis (~> 1.2.0)
+ peek-sidekiq (~> 1.0.3)
+ pg (~> 0.18.2)
+ premailer-rails (~> 1.9.7)
+ prometheus-client-mmap (~> 0.9.1)
+ pry-byebug (~> 3.4.1)
+ pry-rails (~> 0.3.4)
+ rack-attack (~> 4.4.1)
+ rack-cors (~> 1.0.0)
+ rack-oauth2 (~> 1.2.1)
+ rack-proxy (~> 0.6.0)
+ rails (= 5.0.6)
+ rails-deprecated_sanitizer (~> 1.0.3)
+ rails-i18n (~> 5.1)
+ rainbow (~> 2.2)
+ raindrops (~> 0.18)
+ rblineprof (~> 0.3.6)
+ rbnacl (~> 4.0)
+ rbnacl-libsodium
+ rdoc (~> 4.2)
+ re2 (~> 1.1.1)
+ recaptcha (~> 3.0)
+ redcarpet (~> 3.4)
+ redis (~> 3.2)
+ redis-namespace (~> 1.5.2)
+ redis-rails (~> 5.0.2)
+ request_store (~> 1.3)
+ responders (~> 2.0)
+ rouge (~> 2.0)
+ rqrcode-rails3 (~> 0.1.7)
+ rspec-parameterized
+ rspec-rails (~> 3.6.0)
+ rspec-retry (~> 0.4.5)
+ rspec-set (~> 0.1.3)
+ rspec_profiling (~> 0.0.5)
+ rubocop (~> 0.52.1)
+ rubocop-rspec (~> 1.22.1)
+ ruby-fogbugz (~> 0.2.1)
+ ruby-prof (~> 0.17.0)
+ ruby_parser (~> 3.8)
+ rufus-scheduler (~> 3.4)
+ rugged (~> 0.26.0)
+ sanitize (~> 2.0)
+ sass-rails (~> 5.0.6)
+ scss_lint (~> 0.56.0)
+ seed-fu (~> 2.3.7)
+ select2-rails (~> 3.5.9)
+ selenium-webdriver (~> 3.5)
+ sentry-raven (~> 2.7)
+ settingslogic (~> 2.0.9)
+ sham_rack (~> 1.3.6)
+ shoulda-matchers (~> 3.1.2)
+ sidekiq (~> 5.0)
+ sidekiq-cron (~> 0.6.0)
+ sidekiq-limit_fetch (~> 3.4)
+ simple_po_parser (~> 1.1.2)
+ simplecov (~> 0.14.0)
+ slack-notifier (~> 1.5.1)
+ spinach-rails (~> 0.2.1)
+ spinach-rerun-reporter (~> 0.0.2)
+ spring (~> 2.0.0)
+ spring-commands-rspec (~> 1.0.4)
+ spring-commands-spinach (~> 1.1.0)
+ sprockets (~> 3.7.0)
+ sshkey (~> 1.9.0)
+ stackprof (~> 0.2.10)
+ state_machines-activerecord (~> 0.5.1)
+ sys-filesystem (~> 1.1.6)
+ test-prof (~> 0.2.5)
+ test_after_commit (~> 1.1)
+ thin (~> 1.7.0)
+ timecop (~> 0.8.0)
+ toml-rb (~> 1.0.0)
+ truncato (~> 0.7.9)
+ u2f (~> 0.2.1)
+ uglifier (~> 2.7.2)
+ unf (~> 0.1.4)
+ unicorn (~> 5.1.0)
+ unicorn-worker-killer (~> 0.4.4)
+ validates_hostname (~> 1.0.6)
+ version_sorter (~> 2.1.0)
+ virtus (~> 1.0.1)
+ vmstat (~> 2.3.0)
+ webmock (~> 2.3.2)
+ webpack-rails (~> 0.9.10)
+ wikicloth (= 0.8.1)
+
+BUNDLED WITH
+ 1.16.1
diff --git a/Procfile b/Procfile
index cad738d4292..1776fd97942 100644
--- a/Procfile
+++ b/Procfile
@@ -4,4 +4,3 @@
#
web: RAILS_ENV=development bin/web start_foreground
worker: RAILS_ENV=development bin/background_jobs start_foreground
-# mail_room: bundle exec mail_room -q -c config/mail_room.yml
diff --git a/VERSION b/VERSION
index f860e97e4cf..7a86eda5728 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-10.6.0-pre
+10.7.0-pre
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 8d021de7998..84fef4d8b4f 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,6 +1,7 @@
import './autosize';
import './bind_in_out';
-import initCopyAsGFM from './copy_as_gfm';
+import './markdown/render_gfm';
+import initCopyAsGFM from './markdown/copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
diff --git a/app/assets/javascripts/behaviors/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index f5f4f00d587..75cf90de0b5 100644
--- a/app/assets/javascripts/behaviors/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -2,8 +2,8 @@
import $ from 'jquery';
import _ from 'underscore';
-import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils';
-import { placeholderImage } from '../lazy_loader';
+import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils';
+import { placeholderImage } from '~/lazy_loader';
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 94fffcd2f61..dbff2bd4b10 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import syntaxHighlight from '~/syntax_highlight';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
-import syntaxHighlight from './syntax_highlight';
// Render Gitlab flavoured Markdown
//
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 8572bf64d46..7dcf1aeed17 100644
--- a/app/assets/javascripts/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { __ } from './locale';
-import flash from './flash';
+import { __ } from '~/locale';
+import flash from '~/flash';
// Renders math using KaTeX in any element with the
// `js-render-math` class
diff --git a/app/assets/javascripts/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index d4f18955bd2..56b1896e9f1 100644
--- a/app/assets/javascripts/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -1,3 +1,5 @@
+import flash from '~/flash';
+
// Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class.
//
@@ -12,8 +14,6 @@
// </pre>
//
-import Flash from './flash';
-
export default function renderMermaid($els) {
if (!$els.length) return;
@@ -52,6 +52,6 @@ export default function renderMermaid($els) {
});
});
}).catch((err) => {
- Flash(`Can't load mermaid module: ${err}`);
+ flash(`Can't load mermaid module: ${err}`);
});
}
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index 7e882a57202..8aee5b23c76 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
-import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore;
@@ -45,7 +45,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
};
},
components: {
- userAvatarLink,
+ UserAvatarLink,
},
computed: {
numberOverLimit() {
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/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 0932d836589..1638e09132b 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,33 +1,32 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */
-
import $ from 'jquery';
import { rstrip } from './lib/utils/common_utils';
-window.ConfirmDangerModal = (function() {
- function ConfirmDangerModal(form, text) {
- var project_path, submit;
- this.form = form;
- $('.js-confirm-text').text(text || '');
- $('.js-confirm-danger-input').val('');
- $('#modal-confirm-danger').modal('show');
- project_path = $('.js-confirm-danger-match').text();
- submit = $('.js-confirm-danger-submit');
- submit.disable();
- $('.js-confirm-danger-input').off('input');
- $('.js-confirm-danger-input').on('input', function() {
- if (rstrip($(this).val()) === project_path) {
- return submit.enable();
- } else {
- return submit.disable();
- }
- });
- $('.js-confirm-danger-submit').off('click');
- $('.js-confirm-danger-submit').on('click', (function(_this) {
- return function() {
- return _this.form.submit();
- };
- })(this));
- }
+function openConfirmDangerModal($form, text) {
+ $('.js-confirm-text').text(text || '');
+ $('.js-confirm-danger-input').val('');
+ $('#modal-confirm-danger').modal('show');
+
+ const confirmTextMatch = $('.js-confirm-danger-match').text();
+ const $submit = $('.js-confirm-danger-submit');
+ $submit.disable();
+
+ $('.js-confirm-danger-input').off('input').on('input', function handleInput() {
+ const confirmText = rstrip($(this).val());
+ if (confirmText === confirmTextMatch) {
+ $submit.enable();
+ } else {
+ $submit.disable();
+ }
+ });
+ $('.js-confirm-danger-submit').off('click').on('click', () => $form.submit());
+}
- return ConfirmDangerModal;
-})();
+export default function initConfirmDangerModal() {
+ $(document).on('click', '.js-confirm-danger', (e) => {
+ e.preventDefault();
+ const $btn = $(e.target);
+ const $form = $btn.closest('form');
+ const text = $btn.data('confirmDangerMessage');
+ openConfirmDangerModal($form, text);
+ });
+}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 42ecc415173..72f21f13860 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -53,8 +53,12 @@ function initPageShortcuts(page) {
function initGFMInput() {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
- const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
- const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
+ const gfm = new GfmAutoComplete(
+ gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
+ );
+ const enableGFM = convertPermissionToBoolean(
+ el.dataset.supportsAutocomplete,
+ );
gfm.setup($(el), {
emojis: true,
members: enableGFM,
@@ -67,9 +71,9 @@ function initGFMInput() {
}
function initPerformanceBar() {
- if (document.querySelector('#peek')) {
+ if (document.querySelector('#js-peek')) {
import('./performance_bar')
- .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
+ .then(m => new m.default({ container: '#js-peek' })) // eslint-disable-line new-cap
.catch(() => Flash('Error loading performance bar module'));
}
}
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/gl_form.js b/app/assets/javascripts/gl_form.js
index 184c98813f1..9f5eba353d7 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import autosize from 'autosize';
import GfmAutoComplete from './gfm_auto_complete';
import dropzoneInput from './dropzone_input';
-import textUtils from './lib/utils/text_markdown';
+import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
export default class GLForm {
constructor(form, enableGFM = false) {
@@ -47,7 +47,7 @@ export default class GLForm {
}
// form and textarea event listeners
this.addEventListeners();
- textUtils.init(this.form);
+ addMarkdownListeners(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
this.form.show();
@@ -86,7 +86,7 @@ export default class GLForm {
clearEventListeners() {
this.textarea.off('focus');
this.textarea.off('blur');
- textUtils.removeListeners(this.form);
+ removeMarkdownListeners(this.form);
}
addEventListeners() {
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 63bb5832bd0..22eb7bd44c5 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -4,7 +4,7 @@
import $ from 'jquery';
import { s__ } from '~/locale';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
-import modal from '~/vue_shared/components/modal.vue';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -15,7 +15,7 @@ import groupsComponent from './groups.vue';
export default {
components: {
loadingIcon,
- modal,
+ DeprecatedModal,
groupsComponent,
},
props: {
@@ -52,8 +52,9 @@ export default {
},
},
created() {
- this.searchEmptyMessage = this.hideProjects ?
- COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
+ this.searchEmptyMessage = this.hideProjects
+ ? COMMON_STR.GROUP_SEARCH_EMPTY
+ : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
eventHub.$on('fetchPage', this.fetchPage);
eventHub.$on('toggleChildren', this.toggleChildren);
@@ -72,22 +73,30 @@ export default {
eventHub.$off('updateGroups', this.updateGroups);
},
methods: {
- fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) {
- return this.service.getGroups(parentId, page, filterGroupsBy, sortBy, archived)
- .then((res) => {
- if (updatePagination) {
- this.updatePagination(res.headers);
- }
+ fetchGroups({
+ parentId,
+ page,
+ filterGroupsBy,
+ sortBy,
+ archived,
+ updatePagination,
+ }) {
+ return this.service
+ .getGroups(parentId, page, filterGroupsBy, sortBy, archived)
+ .then(res => {
+ if (updatePagination) {
+ this.updatePagination(res.headers);
+ }
- return res;
- })
- .then(res => res.json())
- .catch(() => {
- this.isLoading = false;
- $.scrollTo(0);
+ return res;
+ })
+ .then(res => res.json())
+ .catch(() => {
+ this.isLoading = false;
+ $.scrollTo(0);
- Flash(COMMON_STR.FAILURE);
- });
+ Flash(COMMON_STR.FAILURE);
+ });
},
fetchAllGroups() {
const page = getParameterByName('page') || null;
@@ -103,7 +112,7 @@ export default {
sortBy,
archived,
updatePagination: true,
- }).then((res) => {
+ }).then(res => {
this.isLoading = false;
this.updateGroups(res, Boolean(filterGroupsBy));
});
@@ -118,14 +127,18 @@ export default {
sortBy,
archived,
updatePagination: true,
- }).then((res) => {
+ }).then(res => {
this.isLoading = false;
$.scrollTo(0);
const currentPath = mergeUrlParams({ page }, window.location.href);
- window.history.replaceState({
- page: currentPath,
- }, document.title, currentPath);
+ window.history.replaceState(
+ {
+ page: currentPath,
+ },
+ document.title,
+ currentPath,
+ );
this.updateGroups(res);
});
@@ -138,11 +151,13 @@ export default {
// eslint-disable-next-line promise/catch-or-return
this.fetchGroups({
parentId: parentGroup.id,
- }).then((res) => {
- this.store.setGroupChildren(parentGroup, res);
- }).catch(() => {
- parentGroup.isChildrenLoading = false;
- });
+ })
+ .then(res => {
+ this.store.setGroupChildren(parentGroup, res);
+ })
+ .catch(() => {
+ parentGroup.isChildrenLoading = false;
+ });
} else {
parentGroup.isOpen = true;
}
@@ -154,7 +169,11 @@ export default {
this.targetGroup = group;
this.targetParentGroup = parentGroup;
this.showModal = true;
- this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
+ this.groupLeaveConfirmationMessage = s__(
+ `GroupsTree|Are you sure you want to leave the "${
+ group.fullName
+ }" group?`,
+ );
},
hideLeaveGroupModal() {
this.showModal = false;
@@ -162,14 +181,15 @@ export default {
leaveGroup() {
this.showModal = false;
this.targetGroup.isBeingRemoved = true;
- this.service.leaveGroup(this.targetGroup.leavePath)
+ this.service
+ .leaveGroup(this.targetGroup.leavePath)
.then(res => res.json())
- .then((res) => {
+ .then(res => {
$.scrollTo(0);
this.store.removeGroup(this.targetGroup, this.targetParentGroup);
Flash(res.notice, 'notice');
})
- .catch((err) => {
+ .catch(err => {
let message = COMMON_STR.FAILURE;
if (err.status === 403) {
message = COMMON_STR.LEAVE_FORBIDDEN;
@@ -208,8 +228,8 @@ export default {
:search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
/>
- <modal
- v-if="showModal"
+ <deprecated-modal
+ v-show="showModal"
kind="warning"
:primary-button-label="__('Leave')"
:title="__('Are you sure?')"
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
new file mode 100644
index 00000000000..0c54c992e51
--- /dev/null
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -0,0 +1,31 @@
+<script>
+ import icon from '~/vue_shared/components/icon.vue';
+
+ export default {
+ components: {
+ icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ changedIcon() {
+ return this.file.tempFile ? 'file-addition' : 'file-modified';
+ },
+ changedIconClass() {
+ return `multi-${this.changedIcon}`;
+ },
+ },
+ };
+</script>
+
+<template>
+ <icon
+ :name="changedIcon"
+ :size="12"
+ :css-classes="`ide-file-changed-icon ${changedIconClass}`"
+ />
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
new file mode 100644
index 00000000000..2cbd982af19
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -0,0 +1,65 @@
+<script>
+ import { mapState } from 'vuex';
+ import { sprintf, __ } from '~/locale';
+ import * as consts from '../../stores/modules/commit/constants';
+ import RadioGroup from './radio_group.vue';
+
+ export default {
+ components: {
+ RadioGroup,
+ },
+ computed: {
+ ...mapState([
+ 'currentBranchId',
+ ]),
+ newMergeRequestHelpText() {
+ return sprintf(
+ __('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
+ { branchName: this.currentBranchId },
+ );
+ },
+ commitToCurrentBranchText() {
+ return sprintf(
+ __('Commit to %{branchName} branch'),
+ { branchName: `<strong>${this.currentBranchId}</strong>` },
+ false,
+ );
+ },
+ commitToNewBranchText() {
+ return sprintf(
+ __('Creates a new branch from %{branchName}'),
+ { branchName: this.currentBranchId },
+ );
+ },
+ },
+ commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
+ commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
+ commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
+ };
+</script>
+
+<template>
+ <div class="append-bottom-15 ide-commit-radios">
+ <radio-group
+ :value="$options.commitToCurrentBranch"
+ :checked="true"
+ >
+ <span
+ v-html="commitToCurrentBranchText"
+ >
+ </span>
+ </radio-group>
+ <radio-group
+ :value="$options.commitToNewBranch"
+ :label="__('Create a new branch')"
+ :show-input="true"
+ :help-text="commitToNewBranchText"
+ />
+ <radio-group
+ :value="$options.commitToNewBranchMR"
+ :label="__('Create a new branch and merge request')"
+ :show-input="true"
+ :help-text="newMergeRequestHelpText"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
new file mode 100644
index 00000000000..453208f3f19
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -0,0 +1,66 @@
+<script>
+ import { mapState } from 'vuex';
+ import icon from '~/vue_shared/components/icon.vue';
+ import listItem from './list_item.vue';
+ import listCollapsed from './list_collapsed.vue';
+
+ export default {
+ components: {
+ icon,
+ listItem,
+ listCollapsed,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ fileList: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'currentProjectId',
+ 'currentBranchId',
+ 'rightPanelCollapsed',
+ ]),
+ isCommitInfoShown() {
+ return this.rightPanelCollapsed || this.fileList.length;
+ },
+ },
+ methods: {
+ toggleCollapsed() {
+ this.$emit('toggleCollapsed');
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ :class="{
+ 'multi-file-commit-list': isCommitInfoShown
+ }"
+ >
+ <list-collapsed
+ v-if="rightPanelCollapsed"
+ />
+ <template v-else>
+ <ul
+ v-if="fileList.length"
+ class="list-unstyled append-bottom-0"
+ >
+ <li
+ v-for="file in fileList"
+ :key="file.key"
+ >
+ <list-item
+ :file="file"
+ />
+ </li>
+ </ul>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
new file mode 100644
index 00000000000..15918ac9631
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
@@ -0,0 +1,35 @@
+<script>
+ import { mapGetters } from 'vuex';
+ import icon from '~/vue_shared/components/icon.vue';
+
+ export default {
+ components: {
+ icon,
+ },
+ computed: {
+ ...mapGetters([
+ 'addedFiles',
+ 'modifiedFiles',
+ ]),
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-list-collapsed text-center"
+ >
+ <icon
+ name="file-addition"
+ :size="18"
+ css-classes="multi-file-addition append-bottom-10"
+ />
+ {{ addedFiles.length }}
+ <icon
+ name="file-modified"
+ :size="18"
+ css-classes="multi-file-modified prepend-top-10 append-bottom-10"
+ />
+ {{ modifiedFiles.length }}
+ </div>
+</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
new file mode 100644
index 00000000000..18934af004a
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -0,0 +1,60 @@
+<script>
+ import { mapActions } from 'vuex';
+ import icon from '~/vue_shared/components/icon.vue';
+ import router from '../../ide_router';
+
+ export default {
+ components: {
+ icon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ iconName() {
+ return this.file.tempFile ? 'file-addition' : 'file-modified';
+ },
+ 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}`);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="multi-file-commit-list-item">
+ <button
+ type="button"
+ class="multi-file-commit-list-path"
+ @click="openFileInEditor(file)">
+ <span class="multi-file-commit-list-file-path">
+ <icon
+ :name="iconName"
+ :size="16"
+ :css-classes="iconClass"
+ />{{ file.path }}
+ </span>
+ </button>
+ <button
+ type="button"
+ class="btn btn-blank multi-file-discard-btn"
+ @click="discardFileChanges(file.path)"
+ >
+ Discard
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
new file mode 100644
index 00000000000..4310d762c78
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -0,0 +1,94 @@
+<script>
+ import { mapActions, mapState, mapGetters } from 'vuex';
+ import tooltip from '~/vue_shared/directives/tooltip';
+
+ export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ checked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showInput: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ helpText: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ ...mapState('commit', [
+ 'commitAction',
+ ]),
+ ...mapGetters('commit', [
+ 'newBranchName',
+ ]),
+ },
+ methods: {
+ ...mapActions('commit', [
+ 'updateCommitAction',
+ 'updateBranchName',
+ ]),
+ },
+ };
+</script>
+
+<template>
+ <fieldset>
+ <label>
+ <input
+ type="radio"
+ name="commit-action"
+ :value="value"
+ @change="updateCommitAction($event.target.value)"
+ :checked="checked"
+ v-once
+ />
+ <span class="prepend-left-10">
+ <template v-if="label">
+ {{ label }}
+ </template>
+ <slot v-else></slot>
+ <span
+ v-if="helpText"
+ v-tooltip
+ class="help-block inline"
+ :title="helpText"
+ >
+ <i
+ class="fa fa-question-circle"
+ aria-hidden="true"
+ >
+ </i>
+ </span>
+ </span>
+ </label>
+ <div
+ v-if="commitAction === value && showInput"
+ class="ide-commit-new-branch"
+ >
+ <input
+ type="text"
+ class="form-control"
+ :placeholder="newBranchName"
+ @input="updateBranchName($event.target.value)"
+ />
+ </div>
+ </fieldset>
+</template>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
new file mode 100644
index 00000000000..170347881e0
--- /dev/null
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -0,0 +1,91 @@
+<script>
+ import Icon from '~/vue_shared/components/icon.vue';
+
+ export default {
+ components: {
+ Icon,
+ },
+ props: {
+ hasChanges: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ viewer: {
+ type: String,
+ required: true,
+ },
+ showShadow: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ changeMode(mode) {
+ this.$emit('click', mode);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="dropdown"
+ :class="{
+ shadow: showShadow,
+ }"
+ >
+ <button
+ type="button"
+ class="btn btn-primary btn-sm"
+ :class="{
+ 'btn-inverted': hasChanges,
+ }"
+ data-toggle="dropdown"
+ >
+ <template v-if="viewer === 'editor'">
+ {{ __('Editing') }}
+ </template>
+ <template v-else>
+ {{ __('Reviewing') }}
+ </template>
+ <icon
+ name="angle-down"
+ :size="12"
+ css-classes="caret-down"
+ />
+ </button>
+ <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
+ <ul>
+ <li>
+ <a
+ href="#"
+ @click.prevent="changeMode('editor')"
+ :class="{
+ 'is-active': viewer === 'editor',
+ }"
+ >
+ <strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
+ <span class="dropdown-menu-inner-content">
+ {{ __('View and edit lines') }}
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ href="#"
+ @click.prevent="changeMode('diff')"
+ :class="{
+ 'is-active': viewer === 'diff',
+ }"
+ >
+ <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
+ <span class="dropdown-menu-inner-content">
+ {{ __('Compare changes with the last commit') }}
+ </span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
new file mode 100644
index 00000000000..015e750525a
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -0,0 +1,111 @@
+<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';
+
+ export default {
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ repoFileButtons,
+ ideStatusBar,
+ repoEditor,
+ },
+ props: {
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ committedStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['changedFiles', 'openFiles', 'viewer']),
+ ...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;
+ };
+ },
+ };
+</script>
+
+<template>
+ <div
+ class="ide-view"
+ >
+ <ide-sidebar />
+ <div
+ class="multi-file-edit-pane"
+ >
+ <template
+ v-if="activeFile"
+ >
+ <repo-tabs
+ :files="openFiles"
+ :viewer="viewer"
+ :has-changes="hasChanges"
+ />
+ <repo-editor
+ class="multi-file-edit-pane-content"
+ :file="activeFile"
+ />
+ <repo-file-buttons
+ :file="activeFile"
+ />
+ <ide-status-bar
+ :file="activeFile"
+ />
+ </template>
+ <template
+ v-else
+ >
+ <div
+ v-once
+ class="ide-empty-state"
+ >
+ <div class="row js-empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content svg-250">
+ <img :src="emptyStateSvgPath" />
+ </div>
+ </div>
+ <div class="col-xs-12">
+ <div class="text-content text-center">
+ <h4>
+ Welcome to the GitLab IDE
+ </h4>
+ <p>
+ You can select a file in the left sidebar to begin
+ editing and use the right sidebar to commit your changes.
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </template>
+ </div>
+ <ide-contextbar
+ :no-changes-state-svg-path="noChangesStateSvgPath"
+ :committed-state-svg-path="committedStateSvgPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
new file mode 100644
index 00000000000..79a83b47994
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_context_bar.vue
@@ -0,0 +1,84 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import icon from '~/vue_shared/components/icon.vue';
+import panelResizer from '~/vue_shared/components/panel_resizer.vue';
+import repoCommitSection from './repo_commit_section.vue';
+import ResizablePanel from './resizable_panel.vue';
+
+export default {
+ components: {
+ repoCommitSection,
+ icon,
+ panelResizer,
+ ResizablePanel,
+ },
+ props: {
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ committedStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['changedFiles', 'rightPanelCollapsed']),
+ ...mapGetters(['currentIcon']),
+ },
+ methods: {
+ ...mapActions(['setPanelCollapsedStatus']),
+ },
+};
+</script>
+
+<template>
+ <resizable-panel
+ :collapsible="true"
+ :initial-width="340"
+ side="right"
+ >
+ <div
+ class="multi-file-commit-panel-section"
+ >
+ <header
+ class="multi-file-commit-panel-header"
+ :class="{
+ 'is-collapsed': rightPanelCollapsed,
+ }"
+ >
+ <div
+ class="multi-file-commit-panel-header-title"
+ v-if="!rightPanelCollapsed"
+ >
+ <div
+ v-if="changedFiles.length"
+ >
+ <icon
+ name="list-bulleted"
+ :size="18"
+ />
+ Staged
+ </div>
+ </div>
+ <button
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ @click.stop="setPanelCollapsedStatus({
+ side: 'right',
+ collapsed: !rightPanelCollapsed,
+ })"
+ >
+ <icon
+ :name="currentIcon"
+ :size="18"
+ />
+ </button>
+ </header>
+ <repo-commit-section
+ :no-changes-state-svg-path="noChangesStateSvgPath"
+ :committed-state-svg-path="committedStateSvgPath"
+ />
+ </div>
+ </resizable-panel>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_external_links.vue b/app/assets/javascripts/ide/components/ide_external_links.vue
new file mode 100644
index 00000000000..c6f6e0d2348
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_external_links.vue
@@ -0,0 +1,43 @@
+<script>
+import icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ icon,
+ },
+ props: {
+ projectUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ goBackUrl() {
+ return document.referrer || this.projectUrl;
+ },
+ },
+};
+</script>
+
+<template>
+ <nav
+ class="ide-external-links"
+ v-once
+ >
+ <p>
+ <a
+ :href="goBackUrl"
+ class="ide-sidebar-link"
+ >
+ <icon
+ :size="16"
+ class="append-right-8"
+ name="go-back"
+ />
+ <span class="ide-external-links-text">
+ {{ s__('Go back') }}
+ </span>
+ </a>
+ </p>
+ </nav>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
new file mode 100644
index 00000000000..eb2749e6151
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
@@ -0,0 +1,47 @@
+<script>
+ import icon from '~/vue_shared/components/icon.vue';
+ import repoTree from './ide_repo_tree.vue';
+ import newDropdown from './new_dropdown/index.vue';
+
+ export default {
+ components: {
+ repoTree,
+ icon,
+ newDropdown,
+ },
+ props: {
+ projectId: {
+ type: String,
+ required: true,
+ },
+ branch: {
+ type: Object,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="branch-container">
+ <div class="branch-header">
+ <div class="branch-header-title str-truncated ref-name">
+ <icon
+ name="branch"
+ :size="12"
+ />
+ {{ branch.name }}
+ </div>
+ <div class="branch-header-btns">
+ <new-dropdown
+ :project-id="projectId"
+ :branch="branch.name"
+ path=""
+ />
+ </div>
+ </div>
+ <repo-tree
+ :tree="branch.tree"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue
new file mode 100644
index 00000000000..a6f40286ac1
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_project_tree.vue
@@ -0,0 +1,65 @@
+<script>
+import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
+import Identicon from '../../vue_shared/components/identicon.vue';
+import BranchesTree from './ide_project_branches_tree.vue';
+import ExternalLinks from './ide_external_links.vue';
+
+export default {
+ components: {
+ BranchesTree,
+ ExternalLinks,
+ ProjectAvatarImage,
+ Identicon,
+ },
+ props: {
+ project: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="projects-sidebar">
+ <div class="context-header">
+ <a
+ :title="project.name"
+ :href="project.web_url"
+ >
+ <div
+ v-if="project.avatar_url"
+ class="avatar-container s40 project-avatar"
+ >
+ <project-avatar-image
+ class="avatar-container project-avatar"
+ :link-href="project.path"
+ :img-src="project.avatar_url"
+ :img-alt="project.name"
+ :img-size="40"
+ />
+ </div>
+ <identicon
+ v-else
+ size-class="s40"
+ :entity-id="project.id"
+ :entity-name="project.name"
+ />
+ <div class="sidebar-context-title">
+ {{ project.name }}
+ </div>
+ </a>
+ </div>
+ <external-links
+ :project-url="project.web_url"
+ />
+ <div class="multi-file-commit-panel-inner-scroll">
+ <branches-tree
+ v-for="branch in project.branches"
+ :key="branch.name"
+ :project-id="project.path_with_namespace"
+ :branch="branch"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue
new file mode 100644
index 00000000000..e6af88e04bc
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue
@@ -0,0 +1,41 @@
+<script>
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import RepoFile from './repo_file.vue';
+
+export default {
+ components: {
+ RepoFile,
+ SkeletonLoadingContainer,
+ },
+ props: {
+ tree: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-file-list"
+ >
+ <template v-if="tree.loading">
+ <div
+ class="multi-file-loading-container"
+ v-for="n in 3"
+ :key="n"
+ >
+ <skeleton-loading-container />
+ </div>
+ </template>
+ <template v-else>
+ <repo-file
+ v-for="file in tree.tree"
+ :key="file.key"
+ :file="file"
+ :level="0"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
new file mode 100644
index 00000000000..8cf1ccb4fce
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -0,0 +1,51 @@
+<script>
+ import { mapState, mapGetters } from 'vuex';
+ import icon from '~/vue_shared/components/icon.vue';
+ import panelResizer from '~/vue_shared/components/panel_resizer.vue';
+ import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+ import projectTree from './ide_project_tree.vue';
+ import ResizablePanel from './resizable_panel.vue';
+
+ export default {
+ components: {
+ projectTree,
+ icon,
+ panelResizer,
+ skeletonLoadingContainer,
+ ResizablePanel,
+ },
+ computed: {
+ ...mapState([
+ 'loading',
+ ]),
+ ...mapGetters([
+ 'projectsWithTrees',
+ ]),
+ },
+ };
+</script>
+
+<template>
+ <resizable-panel
+ :collapsible="false"
+ :initial-width="290"
+ side="left"
+ >
+ <div class="multi-file-commit-panel-inner">
+ <template v-if="loading">
+ <div
+ class="multi-file-loading-container"
+ v-for="n in 3"
+ :key="n"
+ >
+ <skeleton-loading-container />
+ </div>
+ </template>
+ <project-tree
+ v-for="project in projectsWithTrees"
+ :key="project.id"
+ :project="project"
+ />
+ </div>
+ </resizable-panel>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
new file mode 100644
index 00000000000..9c386896448
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -0,0 +1,60 @@
+<script>
+ import icon from '~/vue_shared/components/icon.vue';
+ import tooltip from '~/vue_shared/directives/tooltip';
+ import timeAgoMixin from '~/vue_shared/mixins/timeago';
+
+ export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [
+ timeAgoMixin,
+ ],
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="ide-status-bar">
+ <div class="ref-name">
+ <icon
+ name="branch"
+ :size="12"
+ />
+ {{ file.branchId }}
+ </div>
+ <div>
+ <div v-if="file.lastCommit && file.lastCommit.id">
+ Last commit:
+ <a
+ v-tooltip
+ :title="file.lastCommit.message"
+ :href="file.lastCommit.url"
+ >
+ {{ timeFormated(file.lastCommit.updatedAt) }} by
+ {{ file.lastCommit.author }}
+ </a>
+ </div>
+ </div>
+ <div class="text-right">
+ {{ file.name }}
+ </div>
+ <div class="text-right">
+ {{ file.eol }}
+ </div>
+ <div class="text-right">
+ {{ file.editorRow }}:{{ file.editorColumn }}
+ </div>
+ <div class="text-right">
+ {{ file.fileLanguage }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
new file mode 100644
index 00000000000..769e9b79cad
--- /dev/null
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -0,0 +1,111 @@
+<script>
+ import { mapActions } from 'vuex';
+ import icon from '~/vue_shared/components/icon.vue';
+ import newModal from './modal.vue';
+ import upload from './upload.vue';
+
+ export default {
+ components: {
+ icon,
+ newModal,
+ upload,
+ },
+ props: {
+ branch: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ openModal: false,
+ modalType: '',
+ dropdownOpen: false,
+ };
+ },
+ methods: {
+ ...mapActions([
+ 'createTempEntry',
+ ]),
+ createNewItem(type) {
+ this.modalType = type;
+ this.openModal = true;
+ this.dropdownOpen = false;
+ },
+ hideModal() {
+ this.openModal = false;
+ },
+ openDropdown() {
+ this.dropdownOpen = !this.dropdownOpen;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="ide-new-btn">
+ <div
+ class="dropdown"
+ :class="{
+ open: dropdownOpen,
+ }"
+ >
+ <button
+ type="button"
+ class="btn btn-sm btn-default dropdown-toggle add-to-tree"
+ aria-label="Create new file or directory"
+ @click.stop="openDropdown()"
+ >
+ <icon
+ name="plus"
+ :size="12"
+ css-classes="pull-left"
+ />
+ <icon
+ name="arrow-down"
+ :size="12"
+ css-classes="pull-left"
+ />
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right">
+ <li>
+ <a
+ href="#"
+ role="button"
+ @click.stop.prevent="createNewItem('blob')"
+ >
+ {{ __('New file') }}
+ </a>
+ </li>
+ <li>
+ <upload
+ :branch-id="branch"
+ :path="path"
+ @create="createTempEntry"
+ />
+ </li>
+ <li>
+ <a
+ href="#"
+ role="button"
+ @click.stop.prevent="createNewItem('tree')"
+ >
+ {{ __('New directory') }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ <new-modal
+ v-if="openModal"
+ :type="modalType"
+ :branch-id="branch"
+ :path="path"
+ @hide="hideModal"
+ @create="createTempEntry"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
new file mode 100644
index 00000000000..4b5a50785b6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -0,0 +1,99 @@
+<script>
+import { __ } from '~/locale';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+
+export default {
+ components: {
+ DeprecatedModal,
+ },
+ props: {
+ branchId: {
+ type: String,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ entryName: this.path !== '' ? `${this.path}/` : '',
+ };
+ },
+ computed: {
+ modalTitle() {
+ if (this.type === 'tree') {
+ return __('Create new directory');
+ }
+
+ return __('Create new file');
+ },
+ buttonLabel() {
+ if (this.type === 'tree') {
+ return __('Create directory');
+ }
+
+ return __('Create file');
+ },
+ formLabelName() {
+ if (this.type === 'tree') {
+ return __('Directory name');
+ }
+
+ return __('File name');
+ },
+ },
+ mounted() {
+ this.$refs.fieldName.focus();
+ },
+ methods: {
+ createEntryInStore() {
+ this.$emit('create', {
+ branchId: this.branchId,
+ name: this.entryName,
+ type: this.type,
+ });
+
+ this.hideModal();
+ },
+ hideModal() {
+ this.$emit('hide');
+ },
+ },
+};
+</script>
+
+<template>
+ <deprecated-modal
+ :title="modalTitle"
+ :primary-button-label="buttonLabel"
+ kind="success"
+ @cancel="hideModal"
+ @submit="createEntryInStore"
+ >
+ <form
+ class="form-horizontal"
+ slot="body"
+ @submit.prevent="createEntryInStore"
+ >
+ <fieldset class="form-group append-bottom-0">
+ <label class="label-light col-sm-3">
+ {{ formLabelName }}
+ </label>
+ <div class="col-sm-9">
+ <input
+ type="text"
+ class="form-control"
+ v-model="entryName"
+ ref="fieldName"
+ />
+ </div>
+ </fieldset>
+ </form>
+ </deprecated-modal>
+</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
new file mode 100644
index 00000000000..c165af5ce52
--- /dev/null
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -0,0 +1,75 @@
+<script>
+ export default {
+ props: {
+ branchId: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ mounted() {
+ this.$refs.fileUpload.addEventListener('change', this.openFile);
+ },
+ beforeDestroy() {
+ this.$refs.fileUpload.removeEventListener('change', this.openFile);
+ },
+ methods: {
+ createFile(target, file, isText) {
+ const { name } = file;
+ let { result } = target;
+
+ if (!isText) {
+ result = result.split('base64,')[1];
+ }
+
+ this.$emit('create', {
+ name: `${(this.path ? `${this.path}/` : '')}${name}`,
+ branchId: this.branchId,
+ type: 'blob',
+ content: result,
+ base64: !isText,
+ });
+ },
+ readFile(file) {
+ const reader = new FileReader();
+ const isText = file.type.match(/text.*/) !== null;
+
+ reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
+
+ if (isText) {
+ reader.readAsText(file);
+ } else {
+ reader.readAsDataURL(file);
+ }
+ },
+ openFile() {
+ Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
+ },
+ startFileUpload() {
+ this.$refs.fileUpload.click();
+ },
+ },
+ };
+</script>
+
+<template>
+ <div>
+ <a
+ href="#"
+ role="button"
+ @click.stop.prevent="startFileUpload"
+ >
+ {{ __('Upload file') }}
+ </a>
+ <input
+ id="file-upload"
+ type="file"
+ class="hidden"
+ ref="fileUpload"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
new file mode 100644
index 00000000000..d885ed5e301
--- /dev/null
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -0,0 +1,172 @@
+<script>
+import { mapState, mapActions, mapGetters } from 'vuex';
+import tooltip from '~/vue_shared/directives/tooltip';
+import icon from '~/vue_shared/components/icon.vue';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import commitFilesList from './commit_sidebar/list.vue';
+import * as consts from '../stores/modules/commit/constants';
+import Actions from './commit_sidebar/actions.vue';
+
+export default {
+ components: {
+ DeprecatedModal,
+ icon,
+ commitFilesList,
+ Actions,
+ LoadingButton,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ noChangesStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ committedStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'currentProjectId',
+ 'currentBranchId',
+ 'rightPanelCollapsed',
+ 'lastCommitMsg',
+ 'changedFiles',
+ ]),
+ ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
+ ...mapGetters('commit', [
+ 'commitButtonDisabled',
+ 'discardDraftButtonDisabled',
+ 'branchName',
+ ]),
+ statusSvg() {
+ return this.lastCommitMsg
+ ? this.committedStateSvgPath
+ : this.noChangesStateSvgPath;
+ },
+ },
+ methods: {
+ ...mapActions(['setPanelCollapsedStatus']),
+ ...mapActions('commit', [
+ 'updateCommitMessage',
+ 'discardDraft',
+ 'commitChanges',
+ 'updateCommitAction',
+ ]),
+ toggleCollapsed() {
+ this.setPanelCollapsedStatus({
+ side: 'right',
+ collapsed: !this.rightPanelCollapsed,
+ });
+ },
+ forceCreateNewBranch() {
+ return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() =>
+ this.commitChanges(),
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel-section"
+ :class="{
+ 'multi-file-commit-empty-state-container': !changedFiles.length
+ }"
+ >
+ <deprecated-modal
+ id="ide-create-branch-modal"
+ :primary-button-label="__('Create new branch')"
+ kind="success"
+ :title="__('Branch has changed')"
+ @submit="forceCreateNewBranch"
+ >
+ <template slot="body">
+ {{ __(`This branch has changed since you started editing.
+ Would you like to create a new branch?`) }}
+ </template>
+ </deprecated-modal>
+ <commit-files-list
+ title="Staged"
+ :file-list="changedFiles"
+ :collapsed="rightPanelCollapsed"
+ @toggleCollapsed="toggleCollapsed"
+ />
+ <template
+ v-if="changedFiles.length"
+ >
+ <form
+ class="form-horizontal multi-file-commit-form"
+ @submit.prevent.stop="commitChanges"
+ v-if="!rightPanelCollapsed"
+ >
+ <div class="multi-file-commit-fieldset">
+ <textarea
+ class="form-control multi-file-commit-message"
+ name="commit-message"
+ :value="commitMessage"
+ :placeholder="__('Write a commit message...')"
+ @input="updateCommitMessage($event.target.value)"
+ >
+ </textarea>
+ </div>
+ <div class="clearfix prepend-top-15">
+ <actions />
+ <loading-button
+ :loading="submitCommitLoading"
+ :disabled="commitButtonDisabled"
+ container-class="btn btn-success btn-sm pull-left"
+ :label="__('Commit')"
+ @click="commitChanges"
+ />
+ <button
+ v-if="!discardDraftButtonDisabled"
+ type="button"
+ class="btn btn-default btn-sm pull-right"
+ @click="discardDraft"
+ >
+ {{ __('Discard draft') }}
+ </button>
+ </div>
+ </form>
+ </template>
+ <div
+ v-else-if="!rightPanelCollapsed"
+ class="row js-empty-state"
+ >
+ <div class="col-xs-10 col-xs-offset-1">
+ <div class="svg-content svg-80">
+ <img :src="statusSvg" />
+ </div>
+ </div>
+ <div class="col-xs-10 col-xs-offset-1">
+ <div
+ class="text-content text-center"
+ v-if="!lastCommitMsg"
+ >
+ <h4>
+ {{ __('No changes') }}
+ </h4>
+ <p>
+ {{ __('Edit files in the editor and commit changes here') }}
+ </p>
+ </div>
+ <div
+ class="text-content text-center"
+ v-else
+ >
+ <h4>
+ {{ __('All changes are committed') }}
+ </h4>
+ <p v-html="lastCommitMsg">
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
new file mode 100644
index 00000000000..e73d1ce839f
--- /dev/null
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -0,0 +1,161 @@
+<script>
+/* global monaco */
+import { mapState, mapActions } from 'vuex';
+import flash from '~/flash';
+import monacoLoader from '../monaco_loader';
+import Editor from '../lib/editor';
+
+export default {
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'leftPanelCollapsed',
+ 'rightPanelCollapsed',
+ 'viewer',
+ 'delayViewerUpdated',
+ ]),
+ shouldHideEditor() {
+ return this.file && this.file.binary && !this.file.raw;
+ },
+ },
+ watch: {
+ file(oldVal, newVal) {
+ if (newVal.path !== this.file.path) {
+ this.initMonaco();
+ }
+ },
+ leftPanelCollapsed() {
+ this.editor.updateDimensions();
+ },
+ rightPanelCollapsed() {
+ this.editor.updateDimensions();
+ },
+ viewer() {
+ this.createEditorInstance();
+ },
+ },
+ beforeDestroy() {
+ this.editor.dispose();
+ },
+ mounted() {
+ if (this.editor && monaco) {
+ this.initMonaco();
+ } else {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ this.editor = Editor.create(monaco);
+
+ this.initMonaco();
+ });
+ }
+ },
+ methods: {
+ ...mapActions([
+ 'getRawFileData',
+ 'changeFileContent',
+ 'setFileLanguage',
+ 'setEditorPosition',
+ 'setFileEOL',
+ 'updateViewer',
+ 'updateDelayViewerUpdated',
+ ]),
+ initMonaco() {
+ if (this.shouldHideEditor) return;
+
+ this.editor.clearEditor();
+
+ this.getRawFileData(this.file)
+ .then(() => {
+ const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
+
+ return viewerPromise;
+ })
+ .then(() => {
+ this.updateDelayViewerUpdated(false);
+ this.createEditorInstance();
+ })
+ .catch((err) => {
+ flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
+ throw err;
+ });
+ },
+ createEditorInstance() {
+ this.editor.dispose();
+
+ this.$nextTick(() => {
+ if (this.viewer === 'editor') {
+ this.editor.createInstance(this.$refs.editor);
+ } else {
+ this.editor.createDiffInstance(this.$refs.editor);
+ }
+
+ this.setupEditor();
+ });
+ },
+ setupEditor() {
+ if (!this.file || !this.editor.instance) return;
+
+ this.model = this.editor.createModel(this.file);
+
+ this.editor.attachModel(this.model);
+
+ this.model.onChange((model) => {
+ const { file } = model;
+
+ if (file.active) {
+ this.changeFileContent({
+ path: file.path,
+ content: model.getModel().getValue(),
+ });
+ }
+ });
+
+ // Handle Cursor Position
+ this.editor.onPositionChange((instance, e) => {
+ this.setEditorPosition({
+ editorRow: e.position.lineNumber,
+ editorColumn: e.position.column,
+ });
+ });
+
+ this.editor.setPosition({
+ lineNumber: this.file.editorRow,
+ column: this.file.editorColumn,
+ });
+
+ // Handle File Language
+ this.setFileLanguage({
+ fileLanguage: this.model.language,
+ });
+
+ // Get File eol
+ this.setFileEOL({
+ eol: this.model.eol,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ id="ide"
+ class="blob-viewer-container blob-editor-container"
+ >
+ <div
+ v-if="shouldHideEditor"
+ v-html="file.html"
+ >
+ </div>
+ <div
+ v-show="!shouldHideEditor"
+ ref="editor"
+ class="multi-file-editor-holder"
+ >
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
new file mode 100644
index 00000000000..297b9c2628f
--- /dev/null
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -0,0 +1,128 @@
+<script>
+import { mapActions } from 'vuex';
+import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import fileIcon from '~/vue_shared/components/file_icon.vue';
+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';
+
+export default {
+ name: 'RepoFile',
+ components: {
+ skeletonLoadingContainer,
+ newDropdown,
+ fileStatusIcon,
+ fileIcon,
+ changedFileIcon,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ level: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ isTree() {
+ return this.file.type === 'tree';
+ },
+ isBlob() {
+ return this.file.type === 'blob';
+ },
+ levelIndentation() {
+ return {
+ marginLeft: `${this.level * 16}px`,
+ };
+ },
+ fileClass() {
+ return {
+ 'file-open': this.isBlob && this.file.opened,
+ 'file-active': this.isBlob && this.file.active,
+ folder: this.isTree,
+ 'is-open': this.file.opened,
+ };
+ },
+ },
+ updated() {
+ if (this.file.type === 'blob' && this.file.active) {
+ this.$el.scrollIntoView();
+ }
+ },
+ methods: {
+ ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
+ clickFile() {
+ // Manual Action if a tree is selected/opened
+ 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(() => {
+ router.push(`/project${this.file.url}`);
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="file"
+ :class="fileClass"
+ >
+ <div
+ class="file-name"
+ @click="clickFile"
+ role="button"
+ >
+ <span
+ class="ide-file-name str-truncated"
+ :style="levelIndentation"
+ >
+ <file-icon
+ :file-name="file.name"
+ :loading="file.loading"
+ :folder="isTree"
+ :opened="file.opened"
+ :size="16"
+ />
+ {{ file.name }}
+ <file-status-icon
+ :file="file"
+ />
+ </span>
+ <changed-file-icon
+ :file="file"
+ v-if="file.changed || file.tempFile"
+ class="prepend-top-5 pull-right"
+ />
+ <new-dropdown
+ v-if="isTree"
+ :project-id="file.projectId"
+ :branch="file.branchId"
+ :path="file.path"
+ class="pull-right prepend-left-8"
+ />
+ </div>
+ </div>
+ <template v-if="file.opened">
+ <repo-file
+ v-for="childFile in file.tree"
+ :key="childFile.key"
+ :file="childFile"
+ :level="level + 1"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue
new file mode 100644
index 00000000000..4ea8cf7504b
--- /dev/null
+++ b/app/assets/javascripts/ide/components/repo_file_buttons.vue
@@ -0,0 +1,61 @@
+<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
new file mode 100644
index 00000000000..25d311142d5
--- /dev/null
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -0,0 +1,39 @@
+<script>
+ import icon from '~/vue_shared/components/icon.vue';
+ import tooltip from '~/vue_shared/directives/tooltip';
+ import '~/lib/utils/datetime_utility';
+
+ export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ lockTooltip() {
+ return `Locked by ${this.file.file_lock.user.name}`;
+ },
+ },
+ };
+</script>
+
+<template>
+ <span
+ v-if="file.file_lock"
+ v-tooltip
+ :title="lockTooltip"
+ data-container="body"
+ >
+ <icon
+ name="lock"
+ css-classes="file-status-icon"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
new file mode 100644
index 00000000000..79af8c0b0c7
--- /dev/null
+++ b/app/assets/javascripts/ide/components/repo_loading_file.vue
@@ -0,0 +1,42 @@
+<script>
+ import { mapState } from 'vuex';
+ import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+
+ export default {
+ components: {
+ skeletonLoadingContainer,
+ },
+ computed: {
+ ...mapState([
+ 'leftPanelCollapsed',
+ ]),
+ },
+ };
+</script>
+
+<template>
+ <tr
+ class="loading-file"
+ aria-label="Loading files"
+ >
+ <td class="multi-file-table-col-name">
+ <skeleton-loading-container
+ :small="true"
+ />
+ </td>
+ <template v-if="!leftPanelCollapsed">
+ <td class="hidden-sm hidden-xs">
+ <skeleton-loading-container
+ :small="true"
+ />
+ </td>
+
+ <td class="hidden-xs">
+ <skeleton-loading-container
+ class="animation-container-right"
+ :small="true"
+ />
+ </td>
+ </template>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
new file mode 100644
index 00000000000..c337bc813e6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -0,0 +1,98 @@
+<script>
+ 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';
+
+ export default {
+ components: {
+ fileStatusIcon,
+ fileIcon,
+ icon,
+ changedFileIcon,
+ },
+ 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}`;
+ },
+ showChangedIcon() {
+ return this.tab.changed ? !this.tabMouseOver : false;
+ },
+ },
+
+ methods: {
+ ...mapActions([
+ 'closeFile',
+ ]),
+ clickFile(tab) {
+ this.$router.push(`/project${tab.url}`);
+ },
+ mouseOverTab() {
+ if (this.tab.changed) {
+ this.tabMouseOver = true;
+ }
+ },
+ mouseOutTab() {
+ if (this.tab.changed) {
+ this.tabMouseOver = false;
+ }
+ },
+ },
+ };
+</script>
+
+<template>
+ <li
+ @click="clickFile(tab)"
+ @mouseover="mouseOverTab"
+ @mouseout="mouseOutTab"
+ >
+ <button
+ type="button"
+ class="multi-file-tab-close"
+ @click.stop.prevent="closeFile(tab.path)"
+ :aria-label="closeLabel"
+ >
+ <icon
+ v-if="!showChangedIcon"
+ name="close"
+ :size="12"
+ />
+ <changed-file-icon
+ v-else
+ :file="tab"
+ />
+ </button>
+
+ <div
+ class="multi-file-tab"
+ :class="{active : tab.active }"
+ :title="tab.url"
+ >
+ <file-icon
+ :file-name="tab.name"
+ :size="16"
+ />
+ {{ tab.name }}
+ <file-status-icon
+ :file="tab"
+ />
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
new file mode 100644
index 00000000000..8ea64ddf84a
--- /dev/null
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -0,0 +1,61 @@
+<script>
+ import { mapActions } from 'vuex';
+ import RepoTab from './repo_tab.vue';
+ import EditorMode from './editor_mode_dropdown.vue';
+
+ export default {
+ components: {
+ RepoTab,
+ EditorMode,
+ },
+ props: {
+ files: {
+ type: Array,
+ required: true,
+ },
+ viewer: {
+ type: String,
+ required: true,
+ },
+ hasChanges: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showShadow: false,
+ };
+ },
+ updated() {
+ if (!this.$refs.tabsScroller) return;
+
+ this.showShadow =
+ this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
+ },
+ methods: {
+ ...mapActions(['updateViewer']),
+ },
+ };
+</script>
+
+<template>
+ <div class="multi-file-tabs">
+ <ul
+ class="list-unstyled append-bottom-0"
+ ref="tabsScroller"
+ >
+ <repo-tab
+ v-for="tab in files"
+ :key="tab.key"
+ :tab="tab"
+ />
+ </ul>
+ <editor-mode
+ :viewer="viewer"
+ :show-shadow="showShadow"
+ :has-changes="hasChanges"
+ @click="updateViewer"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue
new file mode 100644
index 00000000000..faa690ecba0
--- /dev/null
+++ b/app/assets/javascripts/ide/components/resizable_panel.vue
@@ -0,0 +1,88 @@
+<script>
+ import { mapActions, mapState } from 'vuex';
+ import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+
+ export default {
+ components: {
+ PanelResizer,
+ },
+ props: {
+ collapsible: {
+ type: Boolean,
+ required: true,
+ },
+ initialWidth: {
+ type: Number,
+ required: true,
+ },
+ minSize: {
+ type: Number,
+ required: false,
+ default: 200,
+ },
+ side: {
+ type: String,
+ required: true,
+ },
+ },
+ 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),
+ };
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel"
+ :class="{
+ 'is-collapsed': collapsed && collapsible,
+ }"
+ :style="panelStyle"
+ @click="toggleFullbarCollapsed"
+ >
+ <slot></slot>
+ <panel-resizer
+ :size.sync="width"
+ :enabled="!collapsed"
+ :start-size="initialWidth"
+ :min-size="minSize"
+ :max-size="$options.maxSize"
+ @resize-start="setResizingStatus(true)"
+ @resize-end="setResizingStatus(false)"
+ :side="side === 'right' ? 'left' : 'right'"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/eventhub.js b/app/assets/javascripts/ide/eventhub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/ide/eventhub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
new file mode 100644
index 00000000000..db89c1d44db
--- /dev/null
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -0,0 +1,117 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import flash from '~/flash';
+import store from './stores';
+
+Vue.use(VueRouter);
+
+/**
+ * Routes below /-/ide/:
+
+/project/h5bp/html5-boilerplate/blob/master
+/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
+
+/project/h5bp/html5-boilerplate/mr/123
+/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
+
+/workspace/123
+/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
+/workspace/project/h5bp/html5-boilerplate/mr/123
+
+/ = /workspace
+
+/settings
+*/
+
+// Unfortunately Vue Router doesn't work without at least a fake component
+// If you do only data handling
+const EmptyRouterComponent = {
+ render(createElement) {
+ return createElement('div');
+ },
+};
+
+const router = new VueRouter({
+ mode: 'history',
+ base: `${gon.relative_url_root}/-/ide/`,
+ routes: [
+ {
+ path: '/project/:namespace/:project',
+ component: EmptyRouterComponent,
+ children: [
+ {
+ path: ':targetmode/:branch/*',
+ component: EmptyRouterComponent,
+ },
+ {
+ path: 'mr/:mrid',
+ component: EmptyRouterComponent,
+ },
+ ],
+ },
+ ],
+});
+
+router.beforeEach((to, from, next) => {
+ if (to.params.namespace && to.params.project) {
+ store
+ .dispatch('getProjectData', {
+ namespace: to.params.namespace,
+ projectId: to.params.project,
+ })
+ .then(() => {
+ const fullProjectId = `${to.params.namespace}/${to.params.project}`;
+
+ if (to.params.branch) {
+ store.dispatch('getBranchData', {
+ projectId: fullProjectId,
+ branchId: to.params.branch,
+ });
+
+ store
+ .dispatch('getFiles', {
+ projectId: fullProjectId,
+ branchId: to.params.branch,
+ })
+ .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];
+ if (treeEntry) {
+ store.dispatch('handleTreeEntryAction', treeEntry);
+ }
+ }
+ })
+ .catch(e => {
+ flash(
+ 'Error while loading the branch files. Please try again.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ throw e;
+ });
+ }
+ })
+ .catch(e => {
+ flash(
+ 'Error while loading the project data. Please try again.',
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+ throw e;
+ });
+ }
+
+ next();
+});
+
+export default router;
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
new file mode 100644
index 00000000000..cbfb3dc54f2
--- /dev/null
+++ b/app/assets/javascripts/ide/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import ide from './components/ide.vue';
+import store from './stores';
+import router from './ide_router';
+
+function initIde(el) {
+ if (!el) return null;
+
+ return new Vue({
+ el,
+ store,
+ router,
+ components: {
+ ide,
+ },
+ render(createElement) {
+ return createElement('ide', {
+ props: {
+ emptyStateSvgPath: el.dataset.emptyStateSvgPath,
+ noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
+ committedStateSvgPath: el.dataset.committedStateSvgPath,
+ },
+ });
+ },
+ });
+}
+
+const ideElement = document.getElementById('ide');
+
+Vue.use(Translate);
+
+initIde(ideElement);
diff --git a/app/assets/javascripts/ide/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js
new file mode 100644
index 00000000000..84b29bdb600
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/common/disposable.js
@@ -0,0 +1,14 @@
+export default class Disposable {
+ constructor() {
+ this.disposers = new Set();
+ }
+
+ add(...disposers) {
+ disposers.forEach(disposer => this.disposers.add(disposer));
+ }
+
+ dispose() {
+ this.disposers.forEach(disposer => disposer.dispose());
+ this.disposers.clear();
+ }
+}
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
new file mode 100644
index 00000000000..73cd684351c
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -0,0 +1,90 @@
+/* global monaco */
+import Disposable from './disposable';
+import eventHub from '../../eventhub';
+
+export default class Model {
+ constructor(monaco, file) {
+ this.monaco = monaco;
+ this.disposable = new Disposable();
+ this.file = file;
+ this.content = file.content !== '' ? file.content : file.raw;
+
+ this.disposable.add(
+ (this.originalModel = this.monaco.editor.createModel(
+ this.file.raw,
+ undefined,
+ new this.monaco.Uri(null, null, `original/${this.file.path}`),
+ )),
+ (this.model = this.monaco.editor.createModel(
+ this.content,
+ undefined,
+ new this.monaco.Uri(null, null, 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,
+ );
+ }
+
+ get url() {
+ return this.model.uri.toString();
+ }
+
+ get language() {
+ return this.model.getModeId();
+ }
+
+ get eol() {
+ return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
+ }
+
+ get path() {
+ return this.file.path;
+ }
+
+ getModel() {
+ return this.model;
+ }
+
+ getOriginalModel() {
+ return this.originalModel;
+ }
+
+ setValue(value) {
+ this.getModel().setValue(value);
+ }
+
+ onChange(cb) {
+ this.events.set(
+ this.path,
+ this.disposable.add(this.model.onDidChangeContent(e => cb(this, e))),
+ );
+ }
+
+ updateContent(content) {
+ this.getOriginalModel().setValue(content);
+ this.getModel().setValue(content);
+ }
+
+ dispose() {
+ 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,
+ );
+ }
+}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
new file mode 100644
index 00000000000..57d5e59a88b
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -0,0 +1,51 @@
+import eventHub from '../../eventhub';
+import Disposable from './disposable';
+import Model from './model';
+
+export default class ModelManager {
+ constructor(monaco) {
+ this.monaco = monaco;
+ this.disposable = new Disposable();
+ this.models = new Map();
+ }
+
+ hasCachedModel(path) {
+ return this.models.has(path);
+ }
+
+ getModel(path) {
+ return this.models.get(path);
+ }
+
+ addModel(file) {
+ if (this.hasCachedModel(file.path)) {
+ return this.getModel(file.path);
+ }
+
+ const model = new Model(this.monaco, file);
+ this.models.set(model.path, model);
+ this.disposable.add(model);
+
+ eventHub.$on(
+ `editor.update.model.dispose.${file.path}`,
+ this.removeCachedModel.bind(this, file),
+ );
+
+ return model;
+ }
+
+ removeCachedModel(file) {
+ this.models.delete(file.path);
+
+ eventHub.$off(
+ `editor.update.model.dispose.${file.path}`,
+ this.removeCachedModel,
+ );
+ }
+
+ dispose() {
+ // dispose of all the models
+ this.disposable.dispose();
+ this.models.clear();
+ }
+}
diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
new file mode 100644
index 00000000000..42904774747
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/decorations/controller.js
@@ -0,0 +1,45 @@
+export default class DecorationsController {
+ constructor(editor) {
+ this.editor = editor;
+ this.decorations = new Map();
+ this.editorDecorations = new Map();
+ }
+
+ getAllDecorationsForModel(model) {
+ if (!this.decorations.has(model.url)) return [];
+
+ const modelDecorations = this.decorations.get(model.url);
+ const decorations = [];
+
+ modelDecorations.forEach(val => decorations.push(...val));
+
+ return decorations;
+ }
+
+ addDecorations(model, decorationsKey, decorations) {
+ const decorationMap = this.decorations.get(model.url) || new Map();
+
+ decorationMap.set(decorationsKey, decorations);
+
+ this.decorations.set(model.url, decorationMap);
+
+ this.decorate(model);
+ }
+
+ decorate(model) {
+ if (!this.editor.instance) return;
+
+ const decorations = this.getAllDecorationsForModel(model);
+ const oldDecorations = this.editorDecorations.get(model.url) || [];
+
+ this.editorDecorations.set(
+ model.url,
+ this.editor.instance.deltaDecorations(oldDecorations, decorations),
+ );
+ }
+
+ dispose() {
+ this.decorations.clear();
+ this.editorDecorations.clear();
+ }
+}
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
new file mode 100644
index 00000000000..b136545ad11
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -0,0 +1,72 @@
+/* global monaco */
+import { throttle } from 'underscore';
+import DirtyDiffWorker from './diff_worker';
+import Disposable from '../common/disposable';
+
+export const getDiffChangeType = (change) => {
+ if (change.modified) {
+ return 'modified';
+ } else if (change.added) {
+ return 'added';
+ } else if (change.removed) {
+ return 'removed';
+ }
+
+ return '';
+};
+
+export const getDecorator = change => ({
+ range: new monaco.Range(
+ change.lineNumber,
+ 1,
+ change.endLineNumber,
+ 1,
+ ),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
+ },
+});
+
+export default class DirtyDiffController {
+ constructor(modelManager, decorationsController) {
+ this.disposable = new Disposable();
+ this.editorSimpleWorker = null;
+ this.modelManager = modelManager;
+ this.decorationsController = decorationsController;
+ this.dirtyDiffWorker = new DirtyDiffWorker();
+ this.throttledComputeDiff = throttle(this.computeDiff, 250);
+ this.decorate = this.decorate.bind(this);
+
+ this.dirtyDiffWorker.addEventListener('message', this.decorate);
+ }
+
+ attachModel(model) {
+ model.onChange(() => this.throttledComputeDiff(model));
+ }
+
+ computeDiff(model) {
+ this.dirtyDiffWorker.postMessage({
+ path: model.path,
+ originalContent: model.getOriginalModel().getValue(),
+ newContent: model.getModel().getValue(),
+ });
+ }
+
+ reDecorate(model) {
+ this.decorationsController.decorate(model);
+ }
+
+ decorate({ data }) {
+ const decorations = data.changes.map(change => getDecorator(change));
+ const model = this.modelManager.getModel(data.path);
+ this.decorationsController.addDecorations(model, 'dirtyDiff', decorations);
+ }
+
+ dispose() {
+ this.disposable.dispose();
+
+ this.dirtyDiffWorker.removeEventListener('message', this.decorate);
+ this.dirtyDiffWorker.terminate();
+ }
+}
diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js
new file mode 100644
index 00000000000..0e37f5c4704
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/diff/diff.js
@@ -0,0 +1,30 @@
+import { diffLines } from 'diff';
+
+// eslint-disable-next-line import/prefer-default-export
+export const computeDiff = (originalContent, newContent) => {
+ const changes = diffLines(originalContent, newContent);
+
+ let lineNumber = 1;
+ return changes.reduce((acc, change) => {
+ const findOnLine = acc.find(c => c.lineNumber === lineNumber);
+
+ if (findOnLine) {
+ Object.assign(findOnLine, change, {
+ modified: true,
+ endLineNumber: (lineNumber + change.count) - 1,
+ });
+ } else if ('added' in change || 'removed' in change) {
+ acc.push(Object.assign({}, change, {
+ lineNumber,
+ modified: undefined,
+ endLineNumber: (lineNumber + change.count) - 1,
+ }));
+ }
+
+ if (!change.removed) {
+ lineNumber += change.count;
+ }
+
+ return acc;
+ }, []);
+};
diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
new file mode 100644
index 00000000000..e74c4046330
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js
@@ -0,0 +1,10 @@
+import { computeDiff } from './diff';
+
+self.addEventListener('message', (e) => {
+ const data = e.data;
+
+ self.postMessage({
+ path: data.path,
+ changes: computeDiff(data.originalContent, data.newContent),
+ });
+});
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
new file mode 100644
index 00000000000..887dd7e39b1
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -0,0 +1,168 @@
+import _ from 'underscore';
+import DecorationsController from './decorations/controller';
+import DirtyDiffController from './diff/controller';
+import Disposable from './common/disposable';
+import ModelManager from './common/model_manager';
+import editorOptions, { defaultEditorOptions } from './editor_options';
+import gitlabTheme from './themes/gl_theme';
+
+export const clearDomElement = el => {
+ if (!el || !el.firstChild) return;
+
+ while (el.firstChild) {
+ el.removeChild(el.firstChild);
+ }
+};
+
+export default class Editor {
+ static create(monaco) {
+ if (this.editorInstance) return this.editorInstance;
+
+ this.editorInstance = new Editor(monaco);
+
+ return this.editorInstance;
+ }
+
+ constructor(monaco) {
+ this.monaco = monaco;
+ this.currentModel = null;
+ this.instance = null;
+ this.dirtyDiffController = null;
+ this.disposable = new Disposable();
+ this.modelManager = new ModelManager(this.monaco);
+ this.decorationsController = new DecorationsController(this);
+
+ this.setupMonacoTheme();
+
+ this.debouncedUpdate = _.debounce(() => {
+ this.updateDimensions();
+ }, 200);
+ }
+
+ createInstance(domElement) {
+ if (!this.instance) {
+ clearDomElement(domElement);
+
+ this.disposable.add(
+ (this.instance = this.monaco.editor.create(domElement, {
+ ...defaultEditorOptions,
+ })),
+ (this.dirtyDiffController = new DirtyDiffController(
+ this.modelManager,
+ this.decorationsController,
+ )),
+ );
+
+ window.addEventListener('resize', this.debouncedUpdate, false);
+ }
+ }
+
+ createDiffInstance(domElement) {
+ if (!this.instance) {
+ clearDomElement(domElement);
+
+ this.disposable.add(
+ (this.instance = this.monaco.editor.createDiffEditor(domElement, {
+ ...defaultEditorOptions,
+ readOnly: true,
+ quickSuggestions: false,
+ occurrencesHighlight: false,
+ renderLineHighlight: 'none',
+ hideCursorInOverviewRuler: true,
+ })),
+ );
+
+ window.addEventListener('resize', this.debouncedUpdate, false);
+ }
+ }
+
+ createModel(file) {
+ return this.modelManager.addModel(file);
+ }
+
+ attachModel(model) {
+ if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
+ this.instance.setModel({
+ original: model.getOriginalModel(),
+ modified: model.getModel(),
+ });
+
+ return;
+ }
+
+ this.instance.setModel(model.getModel());
+ if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
+
+ this.currentModel = model;
+
+ this.instance.updateOptions(
+ editorOptions.reduce((acc, obj) => {
+ Object.keys(obj).forEach(key => {
+ Object.assign(acc, {
+ [key]: obj[key](model),
+ });
+ });
+ return acc;
+ }, {}),
+ );
+
+ if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
+ }
+
+ setupMonacoTheme() {
+ this.monaco.editor.defineTheme(
+ gitlabTheme.themeName,
+ gitlabTheme.monacoTheme,
+ );
+
+ this.monaco.editor.setTheme('gitlab');
+ }
+
+ clearEditor() {
+ if (this.instance) {
+ this.instance.setModel(null);
+ }
+ }
+
+ dispose() {
+ window.removeEventListener('resize', this.debouncedUpdate);
+
+ // catch any potential errors with disposing the error
+ // this is mainly for tests caused by elements not existing
+ try {
+ this.disposable.dispose();
+
+ this.instance = null;
+ } catch (e) {
+ this.instance = null;
+
+ if (process.env.NODE_ENV !== 'test') {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+ }
+ }
+
+ updateDimensions() {
+ this.instance.layout();
+ }
+
+ setPosition({ lineNumber, column }) {
+ this.instance.revealPositionInCenter({
+ lineNumber,
+ column,
+ });
+ this.instance.setPosition({
+ lineNumber,
+ column,
+ });
+ }
+
+ onPositionChange(cb) {
+ if (!this.instance.onDidChangeCursorPosition) return;
+
+ this.disposable.add(
+ this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
+ );
+ }
+}
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
new file mode 100644
index 00000000000..a213862f9b3
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -0,0 +1,16 @@
+export const defaultEditorOptions = {
+ model: null,
+ readOnly: false,
+ contextmenu: true,
+ scrollBeyondLastLine: false,
+ minimap: {
+ enabled: false,
+ },
+ wordWrap: 'bounded',
+};
+
+export default [
+ {
+ readOnly: model => !!model.file.file_lock,
+ },
+];
diff --git a/app/assets/javascripts/ide/lib/themes/gl_theme.js b/app/assets/javascripts/ide/lib/themes/gl_theme.js
new file mode 100644
index 00000000000..2fc96250c7d
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/themes/gl_theme.js
@@ -0,0 +1,14 @@
+export default {
+ themeName: 'gitlab',
+ monacoTheme: {
+ base: 'vs',
+ inherit: true,
+ rules: [],
+ colors: {
+ 'editorLineNumber.foreground': '#CCCCCC',
+ 'diffEditor.insertedTextBackground': '#ddfbe6',
+ 'diffEditor.removedTextBackground': '#f9d7dc',
+ 'editor.selectionBackground': '#aad6f8',
+ },
+ },
+};
diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js
new file mode 100644
index 00000000000..142a220097b
--- /dev/null
+++ b/app/assets/javascripts/ide/monaco_loader.js
@@ -0,0 +1,16 @@
+import monacoContext from 'monaco-editor/dev/vs/loader';
+
+monacoContext.require.config({
+ paths: {
+ vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
+ },
+});
+
+// ignore CDN config and use local assets path for service worker which cannot be cross-domain
+const relativeRootPath = (gon && gon.relative_url_root) || '';
+const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`;
+window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` };
+
+// eslint-disable-next-line no-underscore-dangle
+window.__monaco_context__ = monacoContext;
+export default monacoContext.require;
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
new file mode 100644
index 00000000000..5f1fb6cf843
--- /dev/null
+++ b/app/assets/javascripts/ide/services/index.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import Api from '~/api';
+
+Vue.use(VueResource);
+
+export default {
+ getTreeData(endpoint) {
+ return Vue.http.get(endpoint, { params: { format: 'json' } });
+ },
+ getFileData(endpoint) {
+ return Vue.http.get(endpoint, { params: { format: 'json' } });
+ },
+ getRawFileData(file) {
+ if (file.tempFile) {
+ return Promise.resolve(file.content);
+ }
+
+ if (file.raw) {
+ return Promise.resolve(file.raw);
+ }
+
+ return Vue.http.get(file.rawPath, { params: { format: 'json' } })
+ .then(res => res.text());
+ },
+ getProjectData(namespace, project) {
+ return Api.project(`${namespace}/${project}`);
+ },
+ getBranchData(projectId, currentBranchId) {
+ return Api.branchSingle(projectId, currentBranchId);
+ },
+ createBranch(projectId, payload) {
+ const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
+
+ return Vue.http.post(url, payload);
+ },
+ commit(projectId, payload) {
+ return Api.commitMultiple(projectId, payload);
+ },
+ getTreeLastCommit(endpoint) {
+ return Vue.http.get(endpoint, {
+ params: {
+ format: 'json',
+ },
+ });
+ },
+ getFiles(projectUrl, branchId) {
+ const url = `${projectUrl}/files/${branchId}`;
+ return Vue.http.get(url, {
+ params: {
+ format: 'json',
+ },
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
new file mode 100644
index 00000000000..7e920aa9f30
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -0,0 +1,121 @@
+import Vue from 'vue';
+import { visitUrl } from '~/lib/utils/url_utility';
+import flash from '~/flash';
+import * as types from './mutation_types';
+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 discardAllChanges = ({ state, commit, dispatch }) => {
+ state.changedFiles.forEach(file => {
+ commit(types.DISCARD_FILE_CHANGES, file.path);
+
+ if (file.tempFile) {
+ dispatch('closeFile', file.path);
+ }
+ });
+
+ commit(types.REMOVE_ALL_CHANGES_FILES);
+};
+
+export const closeAllFiles = ({ state, dispatch }) => {
+ state.openFiles.forEach(file => dispatch('closeFile', file.path));
+};
+
+export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
+ if (side === 'left') {
+ commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
+ } else {
+ commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
+ }
+};
+
+export const setResizingStatus = ({ commit }, resizing) => {
+ commit(types.SET_RESIZING_STATUS, resizing);
+};
+
+export const createTempEntry = (
+ { state, commit, dispatch },
+ { branchId, name, type, content = '', base64 = false },
+) =>
+ new Promise(resolve => {
+ const worker = new FilesDecoratorWorker();
+ 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.`,
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+
+ resolve();
+
+ return null;
+ }
+
+ worker.addEventListener('message', ({ data }) => {
+ const { file } = data;
+
+ worker.terminate();
+
+ commit(types.CREATE_TMP_ENTRY, {
+ data,
+ projectId: state.currentProjectId,
+ branchId,
+ });
+
+ if (type === 'blob') {
+ commit(types.TOGGLE_FILE_OPEN, file.path);
+ commit(types.ADD_FILE_TO_CHANGED, file.path);
+ dispatch('setFileActive', file.path);
+ }
+
+ resolve(file);
+ });
+
+ worker.postMessage({
+ data: [fullName],
+ projectId: state.currentProjectId,
+ branchId,
+ type,
+ tempFile: true,
+ base64,
+ content,
+ });
+
+ return null;
+ });
+
+export const scrollToTab = () => {
+ Vue.nextTick(() => {
+ const tabs = document.getElementById('tabs');
+
+ if (tabs) {
+ const tabEl = tabs.querySelector('.active .repo-tab');
+
+ tabEl.focus();
+ }
+ });
+};
+
+export const updateViewer = ({ commit }, viewer) => {
+ commit(types.UPDATE_VIEWER, viewer);
+};
+
+export const updateDelayViewerUpdated = ({ commit }, delay) => {
+ commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
+};
+
+export * from './actions/tree';
+export * from './actions/file';
+export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
new file mode 100644
index 00000000000..ddc4b757bf9
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -0,0 +1,146 @@
+import { normalizeHeaders } from '~/lib/utils/common_utils';
+import flash from '~/flash';
+import eventHub from '../../eventhub';
+import service from '../../services';
+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];
+ const fileWasActive = file.active;
+
+ 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}`);
+ } else if (!state.openFiles.length) {
+ router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
+ }
+
+ eventHub.$emit(`editor.update.model.dispose.${file.path}`);
+};
+
+export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
+ const file = state.entries[path];
+ const currentActiveFile = getters.activeFile;
+
+ if (file.active) return;
+
+ if (currentActiveFile) {
+ commit(types.SET_FILE_ACTIVE, {
+ path: currentActiveFile.path,
+ active: false,
+ });
+ }
+
+ commit(types.SET_FILE_ACTIVE, { path, active: true });
+ dispatch('scrollToTab');
+
+ commit(types.SET_CURRENT_PROJECT, file.projectId);
+ commit(types.SET_CURRENT_BRANCH, file.branchId);
+};
+
+export const getFileData = ({ state, commit, dispatch }, file) => {
+ 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_LOADING, { entry: file });
+ })
+ .catch(() => {
+ commit(types.TOGGLE_LOADING, { entry: file });
+ 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 changeFileContent = ({ state, commit }, { path, content }) => {
+ const file = state.entries[path];
+ commit(types.UPDATE_FILE_CONTENT, { path, content });
+
+ const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
+
+ if (file.changed && indexOfChangedFile === -1) {
+ commit(types.ADD_FILE_TO_CHANGED, path);
+ } else if (!file.changed && indexOfChangedFile !== -1) {
+ commit(types.REMOVE_FILE_FROM_CHANGED, path);
+ }
+};
+
+export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
+ if (getters.activeFile) {
+ commit(types.SET_FILE_LANGUAGE, { file: getters.activeFile, fileLanguage });
+ }
+};
+
+export const setFileEOL = ({ getters, commit }, { eol }) => {
+ if (getters.activeFile) {
+ commit(types.SET_FILE_EOL, { file: getters.activeFile, eol });
+ }
+};
+
+export const setEditorPosition = (
+ { getters, commit },
+ { editorRow, editorColumn },
+) => {
+ if (getters.activeFile) {
+ commit(types.SET_FILE_POSITION, {
+ file: getters.activeFile,
+ editorRow,
+ editorColumn,
+ });
+ }
+};
+
+export const discardFileChanges = ({ state, commit }, path) => {
+ const file = state.entries[path];
+
+ commit(types.DISCARD_FILE_CHANGES, path);
+ commit(types.REMOVE_FILE_FROM_CHANGED, path);
+
+ if (file.tempFile && file.opened) {
+ commit(types.TOGGLE_FILE_OPEN, path);
+ }
+
+ eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
+};
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
new file mode 100644
index 00000000000..b3882cb8d21
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -0,0 +1,49 @@
+import flash from '~/flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+
+export const getProjectData = (
+ { commit, state, dispatch },
+ { namespace, projectId, force = false } = {},
+) => new Promise((resolve, reject) => {
+ if (!state.projects[`${namespace}/${projectId}`] || force) {
+ commit(types.TOGGLE_LOADING, { entry: state });
+ service.getProjectData(namespace, projectId)
+ .then(res => res.data)
+ .then((data) => {
+ commit(types.TOGGLE_LOADING, { entry: state });
+ commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
+ if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
+ reject(new Error(`Project not loaded ${namespace}/${projectId}`));
+ });
+ } else {
+ resolve(state.projects[`${namespace}/${projectId}`]);
+ }
+});
+
+export const getBranchData = (
+ { commit, state, dispatch },
+ { projectId, branchId, force = false } = {},
+) => new Promise((resolve, reject) => {
+ if ((typeof state.projects[`${projectId}`] === 'undefined' ||
+ !state.projects[`${projectId}`].branches[branchId])
+ || force) {
+ service.getBranchData(`${projectId}`, branchId)
+ .then(({ data }) => {
+ const { id } = data.commit;
+ commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
+ commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
+ reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
+ });
+ } else {
+ resolve(state.projects[`${projectId}`].branches[branchId]);
+ }
+});
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
new file mode 100644
index 00000000000..70a969a0325
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -0,0 +1,93 @@
+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 FilesDecoratorWorker from '../workers/files_decorator_worker';
+
+export const toggleTreeOpen = ({ commit, dispatch }, path) => {
+ commit(types.TOGGLE_TREE_OPEN, path);
+};
+
+export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
+ if (row.type === 'tree') {
+ dispatch('toggleTreeOpen', row.path);
+ } else if (row.type === 'blob' && (row.opened || row.changed)) {
+ if (row.changed && !row.opened) {
+ commit(types.TOGGLE_FILE_OPEN, row.path);
+ }
+
+ dispatch('setFileActive', row.path);
+ } else {
+ dispatch('getFileData', row);
+ }
+};
+
+export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
+ if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
+
+ 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) => {
+ const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
+
+ if (entry) {
+ commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
+ }
+ });
+
+ dispatch('getLastCommitData', tree);
+ })
+ .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,
+ });
+ })
+ .catch((e) => {
+ flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
+ reject(e);
+ });
+ } else {
+ resolve();
+ }
+});
+
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
new file mode 100644
index 00000000000..eba325a31df
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -0,0 +1,30 @@
+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 projectsWithTrees = state =>
+ Object.keys(state.projects).map(projectId => {
+ const project = state.projects[projectId];
+
+ return {
+ ...project,
+ branches: Object.keys(project.branches).map(branchId => {
+ const branch = project.branches[branchId];
+
+ return {
+ ...branch,
+ tree: state.trees[branch.treeId],
+ };
+ }),
+ };
+ });
+
+// 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;
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
new file mode 100644
index 00000000000..7c82ce7976b
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import commitModule from './modules/commit';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ state: state(),
+ actions,
+ mutations,
+ getters,
+ modules: {
+ commit: commitModule,
+ },
+});
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
new file mode 100644
index 00000000000..f536ce6344b
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -0,0 +1,218 @@
+import $ from 'jquery';
+import { sprintf, __ } from '~/locale';
+import flash from '~/flash';
+import { stripHtml } from '~/lib/utils/text_utility';
+import * as rootTypes from '../../mutation_types';
+import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
+import router from '../../../ide_router';
+import service from '../../../services';
+import * as types from './mutation_types';
+import * as consts from './constants';
+import eventHub from '../../../eventhub';
+
+export const updateCommitMessage = ({ commit }, message) => {
+ commit(types.UPDATE_COMMIT_MESSAGE, message);
+};
+
+export const discardDraft = ({ commit }) => {
+ commit(types.UPDATE_COMMIT_MESSAGE, '');
+};
+
+export const updateCommitAction = ({ commit }, commitAction) => {
+ commit(types.UPDATE_COMMIT_ACTION, commitAction);
+};
+
+export const updateBranchName = ({ commit }, branchName) => {
+ commit(types.UPDATE_NEW_BRANCH_NAME, branchName);
+};
+
+export const setLastCommitMessage = ({ rootState, commit }, data) => {
+ const currentProject = rootState.projects[rootState.currentProjectId];
+ const commitStats = data.stats
+ ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), {
+ additions: data.stats.additions, // eslint-disable-line indent
+ deletions: data.stats.deletions, // eslint-disable-line indent
+ }) // eslint-disable-line indent
+ : '';
+ const commitMsg = sprintf(
+ __('Your changes have been committed. Commit %{commitId} %{commitStats}'),
+ {
+ commitId: `<a href="${currentProject.web_url}/commit/${
+ data.short_id
+ }" class="commit-sha">${data.short_id}</a>`,
+ commitStats,
+ },
+ false,
+ );
+
+ commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true });
+};
+
+export const checkCommitStatus = ({ rootState }) =>
+ service
+ .getBranchData(rootState.currentProjectId, rootState.currentBranchId)
+ .then(({ data }) => {
+ const { id } = data.commit;
+ const selectedBranch =
+ rootState.projects[rootState.currentProjectId].branches[
+ rootState.currentBranchId
+ ];
+
+ if (selectedBranch.workingReference !== id) {
+ return true;
+ }
+
+ return false;
+ })
+ .catch(() =>
+ flash(
+ __('Error checking branch data. Please try again.'),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ ),
+ );
+
+export const updateFilesAfterCommit = (
+ { commit, dispatch, state, rootState, rootGetters },
+ { data, branch },
+) => {
+ const selectedProject = rootState.projects[rootState.currentProjectId];
+ const lastCommit = {
+ commit_path: `${selectedProject.web_url}/commit/${data.id}`,
+ commit: {
+ id: data.id,
+ message: data.message,
+ authored_date: data.committed_date,
+ author_name: data.committer_name,
+ },
+ };
+
+ commit(
+ rootTypes.SET_BRANCH_WORKING_REFERENCE,
+ {
+ projectId: rootState.currentProjectId,
+ branchId: rootState.currentBranchId,
+ reference: data.id,
+ },
+ { root: true },
+ );
+
+ rootState.changedFiles.forEach(entry => {
+ commit(
+ rootTypes.SET_LAST_COMMIT_DATA,
+ {
+ entry,
+ lastCommit,
+ },
+ { root: true },
+ );
+
+ eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
+
+ commit(
+ rootTypes.SET_FILE_RAW_DATA,
+ {
+ file: entry,
+ raw: entry.content,
+ },
+ { root: true },
+ );
+
+ commit(
+ rootTypes.TOGGLE_FILE_CHANGED,
+ {
+ file: entry,
+ changed: false,
+ },
+ { root: true },
+ );
+ });
+
+ commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
+
+ if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) {
+ router.push(
+ `/project/${rootState.currentProjectId}/blob/${branch}/${
+ rootGetters.activeFile.path
+ }`,
+ );
+ }
+
+ dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH);
+};
+
+export const commitChanges = ({
+ commit,
+ state,
+ getters,
+ dispatch,
+ rootState,
+}) => {
+ const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH;
+ const payload = createCommitPayload(
+ getters.branchName,
+ newBranch,
+ state,
+ rootState,
+ );
+ const getCommitStatus = newBranch
+ ? Promise.resolve(false)
+ : dispatch('checkCommitStatus');
+
+ commit(types.UPDATE_LOADING, true);
+
+ return getCommitStatus
+ .then(
+ branchChanged =>
+ new Promise(resolve => {
+ if (branchChanged) {
+ // show the modal with a Bootstrap call
+ $('#ide-create-branch-modal').modal('show');
+ } else {
+ resolve();
+ }
+ }),
+ )
+ .then(() => service.commit(rootState.currentProjectId, payload))
+ .then(({ data }) => {
+ commit(types.UPDATE_LOADING, false);
+
+ if (!data.short_id) {
+ flash(data.message, 'alert', document, null, false, true);
+ return;
+ }
+
+ dispatch('setLastCommitMessage', data);
+ dispatch('updateCommitMessage', '');
+
+ if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR) {
+ dispatch(
+ 'redirectToUrl',
+ createNewMergeRequestUrl(
+ rootState.projects[rootState.currentProjectId].web_url,
+ getters.branchName,
+ rootState.currentBranchId,
+ ),
+ { root: true },
+ );
+ } else {
+ dispatch('updateFilesAfterCommit', {
+ data,
+ branch: getters.branchName,
+ });
+ }
+ })
+ .catch(err => {
+ let errMsg = __('Error committing changes. Please try again.');
+ if (err.response.data && err.response.data.message) {
+ errMsg += ` (${stripHtml(err.response.data.message)})`;
+ }
+ flash(errMsg, 'alert', document, null, false, true);
+ window.dispatchEvent(new Event('resize'));
+
+ commit(types.UPDATE_LOADING, false);
+ });
+};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/constants.js b/app/assets/javascripts/ide/stores/modules/commit/constants.js
new file mode 100644
index 00000000000..230b0a3d9b5
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/commit/constants.js
@@ -0,0 +1,3 @@
+export const COMMIT_TO_CURRENT_BRANCH = '1';
+export const COMMIT_TO_NEW_BRANCH = '2';
+export const COMMIT_TO_NEW_BRANCH_MR = '3';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js
new file mode 100644
index 00000000000..f7cdd6adb0c
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js
@@ -0,0 +1,24 @@
+import * as consts from './constants';
+
+export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading;
+
+export const commitButtonDisabled = (state, getters, rootState) =>
+ getters.discardDraftButtonDisabled || !rootState.changedFiles.length;
+
+export const newBranchName = (state, _, rootState) =>
+ `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr(-5)}`;
+
+export const branchName = (state, getters, rootState) => {
+ if (
+ state.commitAction === consts.COMMIT_TO_NEW_BRANCH ||
+ state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR
+ ) {
+ if (state.newBranchName === '') {
+ return getters.newBranchName;
+ }
+
+ return state.newBranchName;
+ }
+
+ return rootState.currentBranchId;
+};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/index.js b/app/assets/javascripts/ide/stores/modules/commit/index.js
new file mode 100644
index 00000000000..3bf65b02847
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/commit/index.js
@@ -0,0 +1,12 @@
+import state from './state';
+import mutations from './mutations';
+import * as actions from './actions';
+import * as getters from './getters';
+
+export default {
+ namespaced: true,
+ state: state(),
+ mutations,
+ actions,
+ getters,
+};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
new file mode 100644
index 00000000000..9221f054e9f
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js
@@ -0,0 +1,4 @@
+export const UPDATE_COMMIT_MESSAGE = 'UPDATE_COMMIT_MESSAGE';
+export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION';
+export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME';
+export const UPDATE_LOADING = 'UPDATE_LOADING';
diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
new file mode 100644
index 00000000000..797357e3df9
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js
@@ -0,0 +1,24 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.UPDATE_COMMIT_MESSAGE](state, commitMessage) {
+ Object.assign(state, {
+ commitMessage,
+ });
+ },
+ [types.UPDATE_COMMIT_ACTION](state, commitAction) {
+ Object.assign(state, {
+ commitAction,
+ });
+ },
+ [types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) {
+ Object.assign(state, {
+ newBranchName,
+ });
+ },
+ [types.UPDATE_LOADING](state, submitCommitLoading) {
+ Object.assign(state, {
+ submitCommitLoading,
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js
new file mode 100644
index 00000000000..8dae50961b0
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/commit/state.js
@@ -0,0 +1,6 @@
+export default () => ({
+ commitMessage: '',
+ commitAction: '1',
+ newBranchName: '',
+ submitCommitLoading: false,
+});
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
new file mode 100644
index 00000000000..e28f190897c
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -0,0 +1,43 @@
+export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
+export const TOGGLE_LOADING = 'TOGGLE_LOADING';
+export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
+export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
+export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
+export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
+export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
+
+// Project Mutation Types
+export const SET_PROJECT = 'SET_PROJECT';
+export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
+export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
+
+// Branch Mutation Types
+export const SET_BRANCH = 'SET_BRANCH';
+export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
+export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
+
+// Tree mutation types
+export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
+export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
+export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
+export const CREATE_TREE = 'CREATE_TREE';
+export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES';
+
+// File mutation types
+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 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_EOL = 'SET_FILE_EOL';
+export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
+export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
+export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED';
+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 UPDATE_VIEWER = 'UPDATE_VIEWER';
+export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
new file mode 100644
index 00000000000..da41fc9285c
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -0,0 +1,106 @@
+import * as types from './mutation_types';
+import projectMutations from './mutations/project';
+import fileMutations from './mutations/file';
+import treeMutations from './mutations/tree';
+import branchMutations from './mutations/branch';
+
+export default {
+ [types.SET_INITIAL_DATA](state, data) {
+ Object.assign(state, data);
+ },
+ [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,
+ });
+ } else {
+ Object.assign(entry, {
+ loading: forceValue !== undefined ? forceValue : !entry.loading,
+ });
+ }
+ },
+ [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
+ Object.assign(state, {
+ leftPanelCollapsed: collapsed,
+ });
+ },
+ [types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
+ Object.assign(state, {
+ rightPanelCollapsed: collapsed,
+ });
+ },
+ [types.SET_RESIZING_STATUS](state, resizing) {
+ Object.assign(state, {
+ panelResizing: resizing,
+ });
+ },
+ [types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
+ Object.assign(entry.lastCommit, {
+ id: lastCommit.commit.id,
+ url: lastCommit.commit_path,
+ message: lastCommit.commit.message,
+ author: lastCommit.commit.author_name,
+ updatedAt: lastCommit.commit.authored_date,
+ });
+ },
+ [types.SET_LAST_COMMIT_MSG](state, lastCommitMsg) {
+ Object.assign(state, {
+ lastCommitMsg,
+ });
+ },
+ [types.SET_ENTRIES](state, entries) {
+ Object.assign(state, {
+ entries,
+ });
+ },
+ [types.CREATE_TMP_ENTRY](state, { data, projectId, branchId }) {
+ Object.keys(data.entries).reduce((acc, key) => {
+ const entry = data.entries[key];
+ const foundEntry = state.entries[key];
+
+ if (!foundEntry) {
+ Object.assign(state.entries, {
+ [key]: entry,
+ });
+ } else {
+ const tree = entry.tree.filter(
+ f => foundEntry.tree.find(e => e.path === f.path) === undefined,
+ );
+ Object.assign(foundEntry, {
+ tree: foundEntry.tree.concat(tree),
+ });
+ }
+
+ return acc.concat(key);
+ }, []);
+
+ const foundEntry = state.trees[`${projectId}/${branchId}`].tree.find(
+ e => e.path === data.treeList[0].path,
+ );
+
+ if (!foundEntry) {
+ Object.assign(state.trees[`${projectId}/${branchId}`], {
+ tree: state.trees[`${projectId}/${branchId}`].tree.concat(
+ data.treeList,
+ ),
+ });
+ }
+ },
+ [types.UPDATE_VIEWER](state, viewer) {
+ Object.assign(state, {
+ viewer,
+ });
+ },
+ [types.UPDATE_DELAY_VIEWER_CHANGE](state, delayViewerUpdated) {
+ Object.assign(state, {
+ delayViewerUpdated,
+ });
+ },
+ ...projectMutations,
+ ...fileMutations,
+ ...treeMutations,
+ ...branchMutations,
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
new file mode 100644
index 00000000000..2972ba5e38e
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -0,0 +1,26 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_BRANCH](state, currentBranchId) {
+ Object.assign(state, {
+ currentBranchId,
+ });
+ },
+ [types.SET_BRANCH](state, { projectPath, branchName, branch }) {
+ Object.assign(state.projects[projectPath], {
+ branches: {
+ [branchName]: {
+ ...branch,
+ treeId: `${projectPath}/${branchName}`,
+ active: true,
+ workingReference: '',
+ },
+ },
+ });
+ },
+ [types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
+ Object.assign(state.projects[projectId].branches[branchId], {
+ workingReference: reference,
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
new file mode 100644
index 00000000000..2500f13db7c
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -0,0 +1,83 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_FILE_ACTIVE](state, { path, active }) {
+ Object.assign(state.entries[path], {
+ active,
+ });
+ },
+ [types.TOGGLE_FILE_OPEN](state, path) {
+ Object.assign(state.entries[path], {
+ opened: !state.entries[path].opened,
+ });
+
+ if (state.entries[path].opened) {
+ state.openFiles.push(state.entries[path]);
+ } else {
+ Object.assign(state, {
+ openFiles: state.openFiles.filter(f => f.path !== path),
+ });
+ }
+ },
+ [types.SET_FILE_DATA](state, { data, file }) {
+ Object.assign(state.entries[file.path], {
+ id: data.id,
+ blamePath: data.blame_path,
+ commitsPath: data.commits_path,
+ permalink: data.permalink,
+ rawPath: data.raw_path,
+ binary: data.binary,
+ renderError: data.render_error,
+ });
+ },
+ [types.SET_FILE_RAW_DATA](state, { file, raw }) {
+ Object.assign(state.entries[file.path], {
+ raw,
+ });
+ },
+ [types.UPDATE_FILE_CONTENT](state, { path, content }) {
+ const changed = content !== state.entries[path].raw;
+
+ Object.assign(state.entries[path], {
+ content,
+ changed,
+ });
+ },
+ [types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
+ Object.assign(state.entries[file.path], {
+ fileLanguage,
+ });
+ },
+ [types.SET_FILE_EOL](state, { file, eol }) {
+ Object.assign(state.entries[file.path], {
+ eol,
+ });
+ },
+ [types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
+ Object.assign(state.entries[file.path], {
+ editorRow,
+ editorColumn,
+ });
+ },
+ [types.DISCARD_FILE_CHANGES](state, path) {
+ Object.assign(state.entries[path], {
+ content: state.entries[path].raw,
+ changed: false,
+ });
+ },
+ [types.ADD_FILE_TO_CHANGED](state, path) {
+ Object.assign(state, {
+ changedFiles: state.changedFiles.concat(state.entries[path]),
+ });
+ },
+ [types.REMOVE_FILE_FROM_CHANGED](state, path) {
+ Object.assign(state, {
+ changedFiles: state.changedFiles.filter(f => f.path !== path),
+ });
+ },
+ [types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
+ Object.assign(state.entries[file.path], {
+ changed,
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
new file mode 100644
index 00000000000..2816562a919
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -0,0 +1,23 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_PROJECT](state, currentProjectId) {
+ Object.assign(state, {
+ currentProjectId,
+ });
+ },
+ [types.SET_PROJECT](state, { projectPath, project }) {
+ // Add client side properties
+ Object.assign(project, {
+ tree: [],
+ branches: {},
+ active: true,
+ });
+
+ Object.assign(state, {
+ projects: Object.assign({}, state.projects, {
+ [projectPath]: project,
+ }),
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
new file mode 100644
index 00000000000..7f7e470c9bb
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -0,0 +1,38 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.TOGGLE_TREE_OPEN](state, path) {
+ Object.assign(state.entries[path], {
+ opened: !state.entries[path].opened,
+ });
+ },
+ [types.CREATE_TREE](state, { treePath }) {
+ Object.assign(state, {
+ trees: Object.assign({}, state.trees, {
+ [treePath]: {
+ tree: [],
+ loading: true,
+ },
+ }),
+ });
+ },
+ [types.SET_DIRECTORY_DATA](state, { data, treePath }) {
+ Object.assign(state, {
+ trees: Object.assign(state.trees, {
+ [treePath]: {
+ tree: data,
+ },
+ }),
+ });
+ },
+ [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
+ Object.assign(tree, {
+ lastCommitPath: url,
+ });
+ },
+ [types.REMOVE_ALL_CHANGES_FILES](state) {
+ Object.assign(state, {
+ changedFiles: [],
+ });
+ },
+};
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
new file mode 100644
index 00000000000..6110f54951c
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -0,0 +1,19 @@
+export default () => ({
+ currentProjectId: '',
+ currentBranchId: '',
+ changedFiles: [],
+ endpoints: {},
+ lastCommitMsg: '',
+ lastCommitPath: '',
+ loading: false,
+ openFiles: [],
+ parentTreeUrl: '',
+ trees: {},
+ projects: {},
+ leftPanelCollapsed: false,
+ rightPanelCollapsed: false,
+ panelResizing: false,
+ entries: {},
+ viewer: 'editor',
+ delayViewerUpdated: false,
+});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
new file mode 100644
index 00000000000..487ea1ead8e
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -0,0 +1,125 @@
+export const dataStructure = () => ({
+ id: '',
+ key: '',
+ type: '',
+ projectId: '',
+ branchId: '',
+ name: '',
+ url: '',
+ path: '',
+ tempFile: false,
+ tree: [],
+ loading: false,
+ opened: false,
+ active: false,
+ changed: false,
+ lastCommitPath: '',
+ lastCommit: {
+ id: '',
+ url: '',
+ message: '',
+ updatedAt: '',
+ author: '',
+ },
+ blamePath: '',
+ commitsPath: '',
+ permalink: '',
+ rawPath: '',
+ binary: false,
+ html: '',
+ raw: '',
+ content: '',
+ parentTreeUrl: '',
+ renderError: false,
+ base64: false,
+ editorRow: 1,
+ editorColumn: 1,
+ fileLanguage: '',
+ eol: '',
+});
+
+export const decorateData = (entity) => {
+ const {
+ id,
+ projectId,
+ branchId,
+ type,
+ url,
+ name,
+ path,
+ renderError,
+ content = '',
+ tempFile = false,
+ active = false,
+ opened = false,
+ changed = false,
+ parentTreeUrl = '',
+ base64 = false,
+
+ file_lock,
+
+ } = entity;
+
+ return {
+ ...dataStructure(),
+ id,
+ projectId,
+ branchId,
+ key: `${name}-${type}-${id}`,
+ type,
+ name,
+ url,
+ path,
+ tempFile,
+ opened,
+ active,
+ parentTreeUrl,
+ changed,
+ renderError,
+ content,
+ base64,
+
+ file_lock,
+
+ };
+};
+
+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) => {
+ document.title = title;
+};
+
+export const createCommitPayload = (branch, newBranch, state, rootState) => ({
+ branch,
+ commit_message: state.commitMessage,
+ actions: rootState.changedFiles.map(f => ({
+ action: f.tempFile ? 'create' : 'update',
+ file_path: f.path,
+ content: f.content,
+ encoding: f.base64 ? 'base64' : 'text',
+ })),
+ start_branch: newBranch ? rootState.currentBranchId : undefined,
+});
+
+export const createNewMergeRequestUrl = (projectUrl, source, target) =>
+ `${projectUrl}/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}`;
+
+const sortTreesByTypeAndName = (a, b) => {
+ if (a.type === 'tree' && b.type === 'blob') {
+ return -1;
+ } else if (a.type === 'blob' && b.type === 'tree') {
+ return 1;
+ }
+ if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
+ if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
+ return 0;
+};
+
+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
new file mode 100644
index 00000000000..a4cd1ab099f
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js
@@ -0,0 +1,101 @@
+import { decorateData, sortTree } from '../utils';
+
+self.addEventListener('message', e => {
+ const {
+ data,
+ projectId,
+ branchId,
+ tempFile = false,
+ content = '',
+ base64 = false,
+ } = e.data;
+
+ const treeList = [];
+ let file;
+ const entries = data.reduce((acc, path) => {
+ const pathSplit = path.split('/');
+ const blobName = pathSplit.pop().trim();
+
+ if (pathSplit.length > 0) {
+ pathSplit.reduce((pathAcc, folderName) => {
+ const parentFolder = acc[pathAcc[pathAcc.length - 1]];
+ const folderPath = `${
+ parentFolder ? `${parentFolder.path}/` : ''
+ }${folderName}`;
+ const foundEntry = acc[folderPath];
+
+ if (!foundEntry) {
+ const tree = decorateData({
+ projectId,
+ branchId,
+ id: folderPath,
+ name: folderName,
+ path: folderPath,
+ url: `/${projectId}/tree/${branchId}/${folderPath}/`,
+ type: 'tree',
+ parentTreeUrl: parentFolder
+ ? parentFolder.url
+ : `/${projectId}/tree/${branchId}/`,
+ tempFile,
+ changed: tempFile,
+ opened: tempFile,
+ });
+
+ Object.assign(acc, {
+ [folderPath]: tree,
+ });
+
+ if (parentFolder) {
+ parentFolder.tree.push(tree);
+ } else {
+ treeList.push(tree);
+ }
+
+ pathAcc.push(tree.path);
+ } else {
+ pathAcc.push(foundEntry.path);
+ }
+
+ return pathAcc;
+ }, []);
+ }
+
+ if (blobName !== '') {
+ const fileFolder = acc[pathSplit.join('/')];
+ file = decorateData({
+ projectId,
+ branchId,
+ id: path,
+ name: blobName,
+ path,
+ url: `/${projectId}/blob/${branchId}/${path}`,
+ type: 'blob',
+ parentTreeUrl: fileFolder
+ ? fileFolder.url
+ : `/${projectId}/blob/${branchId}`,
+ tempFile,
+ changed: tempFile,
+ content,
+ base64,
+ });
+
+ Object.assign(acc, {
+ [path]: file,
+ });
+
+ if (fileFolder) {
+ fileFolder.tree.push(file);
+ } else {
+ treeList.push(file);
+ }
+ }
+
+ return acc;
+ }, {});
+
+ self.postMessage({
+ entries,
+ treeList: sortTree(treeList),
+ file,
+ });
+});
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 470e3e5c52e..5a16adea4dc 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -1,28 +1,25 @@
/* eslint-disable import/prefer-default-export, func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
-
import $ from 'jquery';
+import { insertText } from '~/lib/utils/common_utils';
-const textUtils = {};
-
-textUtils.selectedText = function(text, textarea) {
+function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
-};
+}
-textUtils.lineBefore = function(text, textarea) {
+function lineBefore(text, textarea) {
var split;
split = text.substring(0, textarea.selectionStart).trim().split('\n');
return split[split.length - 1];
-};
+}
-textUtils.lineAfter = function(text, textarea) {
+function lineAfter(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
-};
+}
-textUtils.blockTagText = function(text, textArea, blockTag, selected) {
- var lineAfter, lineBefore;
- lineBefore = this.lineBefore(text, textArea);
- lineAfter = this.lineAfter(text, textArea);
- if (lineBefore === blockTag && lineAfter === blockTag) {
+function blockTagText(text, textArea, blockTag, selected) {
+ const before = lineBefore(text, textArea);
+ const after = lineAfter(text, textArea);
+ if (before === blockTag && after === blockTag) {
// To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
@@ -32,10 +29,30 @@ textUtils.blockTagText = function(text, textArea, blockTag, selected) {
} else {
return blockTag + "\n" + selected + "\n" + blockTag;
}
-};
+}
-textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
- var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
+function moveCursor(textArea, tag, wrapped, removedLastNewLine) {
+ var pos;
+ if (!textArea.setSelectionRange) {
+ return;
+ }
+ if (textArea.selectionStart === textArea.selectionEnd) {
+ if (wrapped) {
+ pos = textArea.selectionStart - tag.length;
+ } else {
+ pos = textArea.selectionStart;
+ }
+
+ if (removedLastNewLine) {
+ pos -= 1;
+ }
+
+ return textArea.setSelectionRange(pos, pos);
+ }
+}
+
+export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap) {
+ var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
currentLineEmpty = false;
@@ -67,9 +84,9 @@ textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
- insertText = this.blockTagText(text, textArea, blockTag, selected);
+ textToInsert = blockTagText(text, textArea, blockTag, selected);
} else {
- insertText = selectedSplit.map(function(val) {
+ textToInsert = selectedSplit.map(function(val) {
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
@@ -78,78 +95,42 @@ textUtils.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
}).join('\n');
}
} else {
- insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
+ textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
- insertText = '\n' + insertText;
+ textToInsert = '\n' + textToInsert;
}
if (removedLastNewLine) {
- insertText += '\n';
+ textToInsert += '\n';
}
- if (document.queryCommandSupported('insertText')) {
- inserted = document.execCommand('insertText', false, insertText);
- }
- if (!inserted) {
- try {
- document.execCommand("ms-beginUndoUnit");
- } catch (error) {}
- textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
- try {
- document.execCommand("ms-endUndoUnit");
- } catch (error) {}
- }
- return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
-};
+ insertText(textArea, textToInsert);
+ return moveCursor(textArea, tag, wrap, removedLastNewLine);
+}
-textUtils.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
- var pos;
- if (!textArea.setSelectionRange) {
- return;
- }
- if (textArea.selectionStart === textArea.selectionEnd) {
- if (wrapped) {
- pos = textArea.selectionStart - tag.length;
- } else {
- pos = textArea.selectionStart;
- }
-
- if (removedLastNewLine) {
- pos -= 1;
- }
-
- return textArea.setSelectionRange(pos, pos);
- }
-};
-
-textUtils.updateText = function(textArea, tag, blockTag, wrap) {
+function updateText(textArea, tag, blockTag, wrap) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
- selected = this.selectedText(text, textArea);
+ selected = selectedText(text, textArea);
$textArea.focus();
- return this.insertText(textArea, text, tag, blockTag, selected, wrap);
-};
+ return insertMarkdownText(textArea, text, tag, blockTag, selected, wrap);
+}
-textUtils.init = function(form) {
- var self;
- self = this;
+function replaceRange(s, start, end, substitute) {
+ return s.substring(0, start) + substitute + s.substring(end);
+}
+
+export function addMarkdownListeners(form) {
return $('.js-md', form).off('click').on('click', function() {
- var $this;
- $this = $(this);
- return self.updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend'));
+ const $this = $(this);
+ return updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend'));
});
-};
+}
-textUtils.removeListeners = function(form) {
+export function removeMarkdownListeners(form) {
return $('.js-md', form).off('click');
-};
-
-textUtils.replaceRange = function(s, start, end, substitute) {
- return s.substring(0, start) + substitute + s.substring(end);
-};
-
-export default textUtils;
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 870285f7940..2c80baba10b 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,5 +1,4 @@
/* eslint-disable import/first */
-/* global ConfirmDangerModal */
/* global $ */
import jQuery from 'jquery';
@@ -21,7 +20,6 @@ import './behaviors/';
// everything else
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
-import './confirm_danger_modal';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
import initTodoToggle from './header';
@@ -32,7 +30,6 @@ import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import './milestone_select';
import './projects_dropdown';
-import './render_gfm';
import initBreadcrumbs from './breadcrumb';
import initDispatcher from './dispatcher';
@@ -215,16 +212,6 @@ document.addEventListener('DOMContentLoaded', () => {
$(document).trigger('toggle.comments');
});
- $document.on('click', '.js-confirm-danger', (e) => {
- const btn = $(e.target);
- const form = btn.closest('form');
- const text = btn.data('confirmDangerMessage');
- e.preventDefault();
-
- // eslint-disable-next-line no-new
- new ConfirmDangerModal(form, text);
- });
-
$document.on('breakpoint:change', (e, breakpoint) => {
if (breakpoint === 'sm' || breakpoint === 'xs') {
const $gutterIcon = $sidebarGutterToggle.find('i');
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 8ca94ef3e2a..f5572be5fbf 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,158 +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,
- },
- 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>
@@ -188,6 +185,7 @@
:clusters-path="clustersPath"
:empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath"
+ :empty-no-data-svg-path="emptyNoDataSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
/>
</template>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index 9517b8ccb67..c77f451c2d3 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -1,87 +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,
- },
- 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,
+ },
+ emptyGettingStartedSvgPath: {
+ 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
+ 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.emptyUnableToConnectSvgPath,
- 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 659ae575219..b0573510ff9 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -105,6 +105,9 @@ export default class Notes {
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
+ this.$wrapperEl = hasVueMRDiscussionsCookie()
+ ? $(document).find('.diffs')
+ : $(document);
this.cleanBinding();
this.addBinding();
this.setPollingInterval();
@@ -138,10 +141,6 @@ export default class Notes {
}
addBinding() {
- this.$wrapperEl = hasVueMRDiscussionsCookie()
- ? $(document).find('.diffs')
- : $(document);
-
// Edit note link
this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit);
@@ -226,14 +225,9 @@ export default class Notes {
$(window).on('hashchange', this.onHashChange);
this.boundGetContent = this.getContent.bind(this);
document.addEventListener('refreshLegacyNotes', this.boundGetContent);
- this.eventsBound = true;
}
cleanBinding() {
- if (!this.eventsBound) {
- return;
- }
-
this.$wrapperEl.off('click', '.js-note-edit');
this.$wrapperEl.off('click', '.note-edit-cancel');
this.$wrapperEl.off('click', '.js-note-delete');
@@ -1733,6 +1727,7 @@ export default class Notes {
// Get Form metadata
const $submitBtn = $(e.target);
+ $submitBtn.prop('disabled', true);
let $form = $submitBtn.parents('form');
const $closeBtn = $form.find('.js-note-target-close');
const isDiscussionNote =
@@ -1767,7 +1762,6 @@ export default class Notes {
// If comment is to resolve discussion, disable submit buttons while
// comment posting is finished.
if (isDiscussionResolve) {
- $submitBtn.disable();
$form.find('.js-comment-submit-button').disable();
}
@@ -1815,13 +1809,16 @@ 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;
+ $submitBtn.prop('disabled', false);
// Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove();
@@ -1905,7 +1902,7 @@ export default class Notes {
.catch(() => {
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove();
-
+ $submitBtn.prop('disabled', false);
const blurEvent = new CustomEvent('blur.imageDiff', {
detail: e,
});
@@ -1933,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/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/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index 14315d5492e..343c65edb37 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -1,11 +1,11 @@
<script>
import _ from 'underscore';
- import modal from '~/vue_shared/components/modal.vue';
+ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { s__, sprintf } from '~/locale';
export default {
components: {
- modal,
+ DeprecatedModal,
},
props: {
deleteProjectUrl: {
@@ -79,7 +79,7 @@
</script>
<template>
- <modal
+ <deprecated-modal
id="delete-project-modal"
:title="title"
:text="text"
@@ -121,5 +121,5 @@
/>
</form>
</template>
- </modal>
+ </deprecated-modal>
</template>
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 7b5e333011e..0e3ac636661 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -1,11 +1,11 @@
<script>
import _ from 'underscore';
- import modal from '~/vue_shared/components/modal.vue';
+ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { s__, sprintf } from '~/locale';
export default {
components: {
- modal,
+ DeprecatedModal,
},
props: {
deleteUserUrl: {
@@ -113,7 +113,7 @@
</script>
<template>
- <modal
+ <deprecated-modal
id="delete-user-modal"
:title="title"
:text="text"
@@ -170,5 +170,5 @@
{{ secondaryButtonLabel }}
</button>
</template>
- </modal>
+ </deprecated-modal>
</template>
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index d44874c8741..bb91ac84ffb 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -1,7 +1,9 @@
import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
+import initConfirmDangerModal from '~/confirm_danger_modal';
document.addEventListener('DOMContentLoaded', () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
+ initConfirmDangerModal();
});
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index c43e0a0490f..16f792d635a 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -2,14 +2,14 @@
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
- import modal from '~/vue_shared/components/modal.vue';
+ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { n__, s__, sprintf } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import eventHub from '../event_hub';
export default {
components: {
- modal,
+ DeprecatedModal,
},
props: {
issueCount: {
@@ -92,7 +92,7 @@ Once deleted, it cannot be undone or recovered.`),
</script>
<template>
- <modal
+ <deprecated-modal
id="delete-milestone-modal"
:title="title"
:text="text"
@@ -106,5 +106,5 @@ Once deleted, it cannot be undone or recovered.`),
<p v-html="props.text"></p>
</template>
- </modal>
+ </deprecated-modal>
</template>
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/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 064de22dfd6..be37df36be8 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,5 +1,6 @@
import initSettingsPanels from '~/settings_panels';
import setupProjectEdit from '~/project_edit';
+import initConfirmDangerModal from '~/confirm_danger_modal';
import ProjectNew from '../shared/project_new';
import projectAvatar from '../shared/project_avatar';
import initProjectPermissionsSettings from '../shared/permissions';
@@ -11,4 +12,5 @@ document.addEventListener('DOMContentLoaded', () => {
initSettingsPanels();
projectAvatar();
initProjectPermissionsSettings();
+ initConfirmDangerModal();
});
diff --git a/app/assets/javascripts/performance_bar.js b/app/assets/javascripts/performance_bar.js
deleted file mode 100644
index c22598ee665..00000000000
--- a/app/assets/javascripts/performance_bar.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import $ from 'jquery';
-import 'vendor/peek';
-import 'vendor/peek.performance_bar';
-import { getParameterValues } from './lib/utils/url_utility';
-
-export default class PerformanceBar {
- constructor(opts) {
- if (!PerformanceBar.singleton) {
- this.init(opts);
- PerformanceBar.singleton = this;
- }
- return PerformanceBar.singleton;
- }
-
- init(opts) {
- const $container = $(opts.container);
- this.$lineProfileLink = $container.find('.js-toggle-modal-peek-line-profile');
- this.$lineProfileModal = $('#modal-peek-line-profile');
- this.initEventListeners();
- this.showModalOnLoad();
- }
-
- initEventListeners() {
- this.$lineProfileLink.on('click', e => this.handleLineProfileLink(e));
- $(document).on('click', '.js-lineprof-file', PerformanceBar.toggleLineProfileFile);
- }
-
- showModalOnLoad() {
- // When a lineprofiler query-string param is present, we show the line
- // profiler modal upon page load
- if (/lineprofiler/.test(window.location.search)) {
- PerformanceBar.toggleModal(this.$lineProfileModal);
- }
- }
-
- handleLineProfileLink(e) {
- const lineProfilerParameter = getParameterValues('lineprofiler');
- const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`);
- const shouldToggleModal = lineProfilerParameter.length > 0 &&
- lineProfilerParameterRegex.test(e.currentTarget.href);
-
- if (shouldToggleModal) {
- e.preventDefault();
- PerformanceBar.toggleModal(this.$lineProfileModal);
- }
- }
-
- static toggleModal($modal) {
- if ($modal.length) {
- $modal.modal('toggle');
- }
- }
-
- static toggleLineProfileFile(e) {
- $(e.currentTarget).parents('.peek-rblineprof-file').find('.data').toggle();
- }
-}
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
new file mode 100644
index 00000000000..db8a0055acd
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -0,0 +1,93 @@
+<script>
+import GlModal from '~/vue_shared/components/gl_modal.vue';
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ currentRequest: {
+ type: Object,
+ required: true,
+ },
+ metric: {
+ type: String,
+ required: true,
+ },
+ header: {
+ type: String,
+ required: true,
+ },
+ details: {
+ type: String,
+ required: true,
+ },
+ keys: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ metricDetails() {
+ return this.currentRequest.details[this.metric];
+ },
+ detailsList() {
+ return this.metricDetails[this.details];
+ },
+ },
+};
+</script>
+<template>
+ <div
+ :id="`peek-view-${metric}`"
+ class="view"
+ v-if="currentRequest.details"
+ >
+ <button
+ :data-target="`#modal-peek-${metric}-details`"
+ class="btn-blank btn-link bold"
+ type="button"
+ data-toggle="modal"
+ >
+ {{ metricDetails.duration }}
+ /
+ {{ metricDetails.calls }}
+ </button>
+ <gl-modal
+ :id="`modal-peek-${metric}-details`"
+ :header-title-text="header"
+ class="performance-bar-modal"
+ >
+ <table
+ class="table"
+ >
+ <template v-if="detailsList.length">
+ <tr
+ v-for="(item, index) in detailsList"
+ :key="index"
+ >
+ <td><strong>{{ item.duration }}ms</strong></td>
+ <td
+ v-for="key in keys"
+ :key="key"
+ class="break-word"
+ >
+ {{ item[key] }}
+ </td>
+ </tr>
+ </template>
+ <template v-else>
+ <tr>
+ <td>
+ No {{ header.toLowerCase() }} for this request.
+ </td>
+ </tr>
+ </template>
+ </table>
+
+ <div slot="footer">
+ </div>
+ </gl-modal>
+ {{ metric }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
new file mode 100644
index 00000000000..2fd1715ee79
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -0,0 +1,191 @@
+<script>
+import $ from 'jquery';
+
+import PerformanceBarService from '../services/performance_bar_service';
+import detailedMetric from './detailed_metric.vue';
+import requestSelector from './request_selector.vue';
+import simpleMetric from './simple_metric.vue';
+import upstreamPerformanceBar from './upstream_performance_bar.vue';
+
+import Flash from '../../flash';
+
+export default {
+ components: {
+ detailedMetric,
+ requestSelector,
+ simpleMetric,
+ upstreamPerformanceBar,
+ },
+ props: {
+ store: {
+ type: Object,
+ required: true,
+ },
+ env: {
+ type: String,
+ required: true,
+ },
+ requestId: {
+ type: String,
+ required: true,
+ },
+ peekUrl: {
+ type: String,
+ required: true,
+ },
+ profileUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ detailedMetrics: [
+ { metric: 'pg', header: 'SQL queries', details: 'queries', keys: ['sql'] },
+ {
+ metric: 'gitaly',
+ header: 'Gitaly calls',
+ details: 'details',
+ keys: ['feature', 'request'],
+ },
+ ],
+ simpleMetrics: ['redis', 'sidekiq'],
+ data() {
+ return { currentRequestId: '' };
+ },
+ computed: {
+ requests() {
+ return this.store.requestsWithDetails();
+ },
+ currentRequest: {
+ get() {
+ return this.store.findRequest(this.currentRequestId);
+ },
+ set(requestId) {
+ this.currentRequestId = requestId;
+ },
+ },
+ initialRequest() {
+ return this.currentRequestId === this.requestId;
+ },
+ lineProfileModal() {
+ return $('#modal-peek-line-profile');
+ },
+ },
+ mounted() {
+ this.interceptor = PerformanceBarService.registerInterceptor(
+ this.peekUrl,
+ this.loadRequestDetails,
+ );
+
+ this.loadRequestDetails(this.requestId, window.location.href);
+ this.currentRequest = this.requestId;
+
+ if (this.lineProfileModal.length) {
+ this.lineProfileModal.modal('toggle');
+ }
+ },
+ beforeDestroy() {
+ PerformanceBarService.removeInterceptor(this.interceptor);
+ },
+ methods: {
+ loadRequestDetails(requestId, requestUrl) {
+ if (!this.store.canTrackRequest(requestUrl)) {
+ return;
+ }
+
+ this.store.addRequest(requestId, requestUrl);
+
+ PerformanceBarService.fetchRequestDetails(this.peekUrl, requestId)
+ .then(res => {
+ this.store.addRequestDetails(requestId, res.data.data);
+ })
+ .catch(() =>
+ Flash(`Error getting performance bar results for ${requestId}`),
+ );
+ },
+ changeCurrentRequest(newRequestId) {
+ this.currentRequest = newRequestId;
+ },
+ },
+};
+</script>
+<template>
+ <div
+ id="js-peek"
+ :class="env"
+ >
+ <div
+ v-if="currentRequest"
+ class="container-fluid container-limited"
+ >
+ <div
+ id="peek-view-host"
+ class="view"
+ >
+ <span
+ v-if="currentRequest.details"
+ class="current-host"
+ >
+ {{ currentRequest.details.host.hostname }}
+ </span>
+ </div>
+ <upstream-performance-bar
+ v-if="initialRequest && currentRequest.details"
+ />
+ <detailed-metric
+ v-for="metric in $options.detailedMetrics"
+ :key="metric.metric"
+ :current-request="currentRequest"
+ :metric="metric.metric"
+ :header="metric.header"
+ :details="metric.details"
+ :keys="metric.keys"
+ />
+ <div
+ v-if="initialRequest"
+ id="peek-view-rblineprof"
+ class="view"
+ >
+ <button
+ v-if="lineProfileModal.length"
+ class="btn-link btn-blank"
+ data-toggle="modal"
+ data-target="#modal-peek-line-profile"
+ >
+ profile
+ </button>
+ <a
+ v-else
+ :href="profileUrl"
+ >
+ profile
+ </a>
+ </div>
+ <simple-metric
+ v-for="metric in $options.simpleMetrics"
+ :current-request="currentRequest"
+ :key="metric"
+ :metric="metric"
+ />
+ <div
+ id="peek-view-gc"
+ class="view"
+ >
+ <span
+ v-if="currentRequest.details"
+ class="bold"
+ >
+ <span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span>ms
+ /
+ <span title="Invoke Count">{{ currentRequest.details.gc.invokes }}</span>
+ gc
+ </span>
+ </div>
+ <request-selector
+ v-if="currentRequest"
+ :current-request="currentRequest"
+ :requests="requests"
+ @change-current-request="changeCurrentRequest"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
new file mode 100644
index 00000000000..3ed07a4a47d
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -0,0 +1,52 @@
+<script>
+export default {
+ props: {
+ currentRequest: {
+ type: Object,
+ required: true,
+ },
+ requests: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ currentRequestId: this.currentRequest.id,
+ };
+ },
+ watch: {
+ currentRequestId(newRequestId) {
+ this.$emit('change-current-request', newRequestId);
+ },
+ },
+ methods: {
+ truncatedUrl(requestUrl) {
+ const components = requestUrl.replace(/\/$/, '').split('/');
+ let truncated = components[components.length - 1];
+
+ if (truncated.match(/^\d+$/)) {
+ truncated = `${components[components.length - 2]}/${truncated}`;
+ }
+
+ return truncated;
+ },
+ },
+};
+</script>
+<template>
+ <div
+ id="peek-request-selector"
+ class="pull-right"
+ >
+ <select v-model="currentRequestId">
+ <option
+ v-for="request in requests"
+ :key="request.id"
+ :value="request.id"
+ >
+ {{ truncatedUrl(request.url) }}
+ </option>
+ </select>
+ </div>
+</template>
diff --git a/app/assets/javascripts/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue
new file mode 100644
index 00000000000..b654bc66249
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue
@@ -0,0 +1,30 @@
+<script>
+export default {
+ props: {
+ currentRequest: {
+ type: Object,
+ required: true,
+ },
+ metric: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div
+ :id="`peek-view-${metric}`"
+ class="view"
+ >
+ <span
+ v-if="currentRequest.details"
+ class="bold"
+ >
+ {{ currentRequest.details[metric].duration }}
+ /
+ {{ currentRequest.details[metric].calls }}
+ </span>
+ {{ metric }}
+ </div>
+</template>
diff --git a/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue
new file mode 100644
index 00000000000..2b5915f381f
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/components/upstream_performance_bar.vue
@@ -0,0 +1,20 @@
+<script>
+export default {
+ mounted() {
+ const upstreamPerformanceBar = document
+ .getElementById('peek-view-performance-bar')
+ .cloneNode(true);
+
+ upstreamPerformanceBar.classList.remove('hidden');
+
+ this.$refs.wrapper.appendChild(upstreamPerformanceBar);
+ },
+};
+</script>
+<template>
+ <div
+ id="peek-view-performance-bar-vue"
+ class="view"
+ ref="wrapper"
+ ></div>
+</template>
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
new file mode 100644
index 00000000000..a0ddf36a672
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -0,0 +1,37 @@
+import 'vendor/peek.performance_bar';
+
+import Vue from 'vue';
+import performanceBarApp from './components/performance_bar_app.vue';
+import PerformanceBarStore from './stores/performance_bar_store';
+
+export default ({ container }) =>
+ new Vue({
+ el: container,
+ components: {
+ performanceBarApp,
+ },
+ data() {
+ const performanceBarData = document.querySelector(this.$options.el)
+ .dataset;
+ const store = new PerformanceBarStore();
+
+ return {
+ store,
+ env: performanceBarData.env,
+ requestId: performanceBarData.requestId,
+ peekUrl: performanceBarData.peekUrl,
+ profileUrl: performanceBarData.profileUrl,
+ };
+ },
+ render(createElement) {
+ return createElement('performance-bar-app', {
+ props: {
+ store: this.store,
+ env: this.env,
+ requestId: this.requestId,
+ peekUrl: this.peekUrl,
+ profileUrl: this.profileUrl,
+ },
+ });
+ },
+ });
diff --git a/app/assets/javascripts/performance_bar/services/performance_bar_service.js b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
new file mode 100644
index 00000000000..3ebfaa87a4e
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/services/performance_bar_service.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import _ from 'underscore';
+import axios from '../../lib/utils/axios_utils';
+
+let vueResourceInterceptor;
+
+export default class PerformanceBarService {
+ static fetchRequestDetails(peekUrl, requestId) {
+ return axios.get(peekUrl, { params: { request_id: requestId } });
+ }
+
+ static registerInterceptor(peekUrl, callback) {
+ vueResourceInterceptor = (request, next) => {
+ next(response => {
+ const requestId = response.headers['x-request-id'];
+ const requestUrl = response.url;
+
+ if (requestUrl !== peekUrl && requestId) {
+ callback(requestId, requestUrl);
+ }
+ });
+ };
+
+ Vue.http.interceptors.push(vueResourceInterceptor);
+
+ return axios.interceptors.response.use(response => {
+ const requestId = response.headers['x-request-id'];
+ const requestUrl = response.config.url;
+
+ if (requestUrl !== peekUrl && requestId) {
+ callback(requestId, requestUrl);
+ }
+
+ return response;
+ });
+ }
+
+ static removeInterceptor(interceptor) {
+ axios.interceptors.response.eject(interceptor);
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors,
+ vueResourceInterceptor,
+ );
+ }
+}
diff --git a/app/assets/javascripts/performance_bar/stores/performance_bar_store.js b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
new file mode 100644
index 00000000000..c6b2f55243c
--- /dev/null
+++ b/app/assets/javascripts/performance_bar/stores/performance_bar_store.js
@@ -0,0 +1,39 @@
+export default class PerformanceBarStore {
+ constructor() {
+ this.requests = [];
+ }
+
+ addRequest(requestId, requestUrl, requestDetails) {
+ if (!this.findRequest(requestId)) {
+ this.requests.push({
+ id: requestId,
+ url: requestUrl,
+ details: requestDetails,
+ });
+ }
+
+ return this.requests;
+ }
+
+ findRequest(requestId) {
+ return this.requests.find(request => request.id === requestId);
+ }
+
+ addRequestDetails(requestId, requestDetails) {
+ const request = this.findRequest(requestId);
+
+ request.details = requestDetails;
+
+ return request;
+ }
+
+ requestsWithDetails() {
+ return this.requests.filter(request => request.details);
+ }
+
+ canTrackRequest(requestUrl) {
+ return (
+ this.requests.filter(request => request.url === requestUrl).length < 2
+ );
+ }
+}
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index c9028952ddd..714aed1333e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -1,5 +1,5 @@
<script>
- import modal from '~/vue_shared/components/modal.vue';
+ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { s__, sprintf } from '~/locale';
import pipelinesTableRowComponent from './pipelines_table_row.vue';
import eventHub from '../event_hub';
@@ -12,7 +12,7 @@
export default {
components: {
pipelinesTableRowComponent,
- modal,
+ DeprecatedModal,
},
props: {
pipelines: {
@@ -120,7 +120,7 @@
:auto-devops-help-path="autoDevopsHelpPath"
:view-type="viewType"
/>
- <modal
+ <deprecated-modal
id="confirmation-modal"
:title="modalTitle"
:text="modalText"
@@ -134,6 +134,6 @@
>
<p v-html="props.text"></p>
</template>
- </modal>
+ </deprecated-modal>
</div>
</template>
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 1ffe482d782..f50002afbf2 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -1,11 +1,11 @@
<script>
- import modal from '~/vue_shared/components/modal.vue';
+ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { __, s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
export default {
components: {
- modal,
+ DeprecatedModal,
},
props: {
actionUrl: {
@@ -76,7 +76,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
</script>
<template>
- <modal
+ <deprecated-modal
id="delete-account-modal"
:title="s__('Profiles|Delete your account?')"
:text="text"
@@ -131,5 +131,5 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
</form>
</template>
- </modal>
+ </deprecated-modal>
</template>
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 3c1bef23446..0af34657d72 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,7 +1,6 @@
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
import $ from 'jquery';
-import Cookies from 'js-cookie';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import flash from '../flash';
@@ -10,7 +9,6 @@ export default class Profile {
constructor({ form } = {}) {
this.onSubmitForm = this.onSubmitForm.bind(this);
this.form = form || $('.edit-user');
- this.newRepoActivated = Cookies.get('new_repo');
this.setRepoRadio();
this.bindEvents();
this.initAvatarGlCrop();
@@ -23,21 +21,28 @@ export default class Profile {
modalCrop: '.modal-profile-crop',
pickImageEl: '.js-choose-user-avatar-button',
uploadImageBtn: '.js-upload-user-avatar',
- modalCropImg: '.modal-profile-crop-image'
+ modalCropImg: '.modal-profile-crop-image',
};
- this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
+ this.avatarGlCrop = $('.js-user-avatar-input')
+ .glCrop(cropOpts)
+ .data('glcrop');
}
bindEvents() {
- $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
- $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
+ $('.js-preferences-form').on(
+ 'change.preference',
+ 'input[type=radio]',
+ this.submitForm,
+ );
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
}
submitForm() {
- return $(this).parents('form').submit();
+ return $(this)
+ .parents('form')
+ .submit();
}
onSubmitForm(e) {
@@ -59,21 +64,13 @@ export default class Profile {
url: this.form.attr('action'),
data: formData,
})
- .then(({ data }) => flash(data.message, 'notice'))
- .then(() => {
- window.scrollTo(0, 0);
- // Enable submit button after requests ends
- self.form.find(':input[disabled]').enable();
- })
- .catch(error => flash(error.message));
- }
-
- setNewRepoCookie() {
- if (this.value === 'off') {
- Cookies.remove('new_repo');
- } else {
- Cookies.set('new_repo', true, { expires_in: 365 });
- }
+ .then(({ data }) => flash(data.message, 'notice'))
+ .then(() => {
+ window.scrollTo(0, 0);
+ // Enable submit button after requests ends
+ self.form.find(':input[disabled]').enable();
+ })
+ .catch(error => flash(error.message));
}
setRepoRadio() {
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 3031230277d..193788f754f 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -3,7 +3,7 @@ import Mousetrap from 'mousetrap';
import _ from 'underscore';
import Sidebar from './right_sidebar';
import Shortcuts from './shortcuts';
-import { CopyAsGFM } from './behaviors/copy_as_gfm';
+import { CopyAsGFM } from './behaviors/markdown/copy_as_gfm';
export default class ShortcutsIssuable extends Shortcuts {
constructor(isMergeRequest) {
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
deleted file mode 100644
index a9fbc7f1a2f..00000000000
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import stopwatchSvg from 'icons/_icon_stopwatch.svg';
-import { abbreviateTime } from '../../../lib/utils/pretty_time';
-
-export default {
- name: 'time-tracking-collapsed-state',
- props: {
- showComparisonState: {
- type: Boolean,
- required: true,
- },
- showSpentOnlyState: {
- type: Boolean,
- required: true,
- },
- showEstimateOnlyState: {
- type: Boolean,
- required: true,
- },
- showNoTimeTrackingState: {
- type: Boolean,
- required: true,
- },
- timeSpentHumanReadable: {
- type: String,
- required: false,
- default: '',
- },
- timeEstimateHumanReadable: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- timeSpent() {
- return this.abbreviateTime(this.timeSpentHumanReadable);
- },
- timeEstimate() {
- return this.abbreviateTime(this.timeEstimateHumanReadable);
- },
- divClass() {
- if (this.showComparisonState) {
- return 'compare';
- } else if (this.showEstimateOnlyState) {
- return 'estimate-only';
- } else if (this.showSpentOnlyState) {
- return 'spend-only';
- } else if (this.showNoTimeTrackingState) {
- return 'no-tracking';
- }
-
- return '';
- },
- spanClass() {
- if (this.showComparisonState) {
- return '';
- } else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
- return 'bold';
- } else if (this.showNoTimeTrackingState) {
- return 'no-value';
- }
-
- return '';
- },
- text() {
- if (this.showComparisonState) {
- return `${this.timeSpent} / ${this.timeEstimate}`;
- } else if (this.showEstimateOnlyState) {
- return `-- / ${this.timeEstimate}`;
- } else if (this.showSpentOnlyState) {
- return `${this.timeSpent} / --`;
- } else if (this.showNoTimeTrackingState) {
- return 'None';
- }
-
- return '';
- },
- },
- methods: {
- abbreviateTime(timeStr) {
- return abbreviateTime(timeStr);
- },
- },
- template: `
- <div class="sidebar-collapsed-icon">
- ${stopwatchSvg}
- <div class="time-tracking-collapsed-summary">
- <div :class="divClass">
- <span :class="spanClass">
- {{ text }}
- </span>
- </div>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
new file mode 100644
index 00000000000..3b86f1145d1
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -0,0 +1,102 @@
+<script>
+ import icon from '../../../vue_shared/components/icon.vue';
+ import { abbreviateTime } from '../../../lib/utils/pretty_time';
+
+ export default {
+ name: 'TimeTrackingCollapsedState',
+ components: {
+ icon,
+ },
+ props: {
+ showComparisonState: {
+ type: Boolean,
+ required: true,
+ },
+ showSpentOnlyState: {
+ type: Boolean,
+ required: true,
+ },
+ showEstimateOnlyState: {
+ type: Boolean,
+ required: true,
+ },
+ showNoTimeTrackingState: {
+ type: Boolean,
+ required: true,
+ },
+ timeSpentHumanReadable: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ timeEstimateHumanReadable: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ timeSpent() {
+ return this.abbreviateTime(this.timeSpentHumanReadable);
+ },
+ timeEstimate() {
+ return this.abbreviateTime(this.timeEstimateHumanReadable);
+ },
+ divClass() {
+ if (this.showComparisonState) {
+ return 'compare';
+ } else if (this.showEstimateOnlyState) {
+ return 'estimate-only';
+ } else if (this.showSpentOnlyState) {
+ return 'spend-only';
+ } else if (this.showNoTimeTrackingState) {
+ return 'no-tracking';
+ }
+
+ return '';
+ },
+ spanClass() {
+ if (this.showComparisonState) {
+ return '';
+ } else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
+ return 'bold';
+ } else if (this.showNoTimeTrackingState) {
+ return 'no-value';
+ }
+
+ return '';
+ },
+ text() {
+ if (this.showComparisonState) {
+ return `${this.timeSpent} / ${this.timeEstimate}`;
+ } else if (this.showEstimateOnlyState) {
+ return `-- / ${this.timeEstimate}`;
+ } else if (this.showSpentOnlyState) {
+ return `${this.timeSpent} / --`;
+ } else if (this.showNoTimeTrackingState) {
+ return 'None';
+ }
+
+ return '';
+ },
+ },
+ methods: {
+ abbreviateTime(timeStr) {
+ return abbreviateTime(timeStr);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="sidebar-collapsed-icon">
+ <icon name="timer" />
+ <div class="time-tracking-collapsed-summary">
+ <div :class="divClass">
+ <span :class="spanClass">
+ {{ text }}
+ </span>
+ </div>
+ </div>
+ </div>
+</template>
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 230736a56b8..1c641c73ea3 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -1,21 +1,21 @@
<script>
import timeTrackingHelpState from './help_state';
-import timeTrackingCollapsedState from './collapsed_state';
+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';
export default {
name: 'IssuableTimeTracker',
components: {
- 'time-tracking-collapsed-state': timeTrackingCollapsedState,
+ TimeTrackingCollapsedState,
'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/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
index a16f9055a6d..95c8b0a4c55 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue
@@ -1,4 +1,5 @@
<script>
+import { sprintf, s__ } from '~/locale';
import statusCodes from '../../lib/utils/http_status';
import { bytesToMiB } from '../../lib/utils/number_utils';
import { backOff } from '../../lib/utils/common_utils';
@@ -45,17 +46,28 @@ export default {
shouldShowMetricsUnavailable() {
return !this.loadingMetrics && !this.hasMetrics && !this.loadFailed;
},
- memoryChangeType() {
+ memoryChangeMessage() {
+ const messageProps = {
+ memoryFrom: this.memoryFrom,
+ memoryTo: this.memoryTo,
+ metricsLinkStart: `<a href="${this.metricsMonitoringUrl}">`,
+ metricsLinkEnd: '</a>',
+ emphasisStart: '<b>',
+ emphasisEnd: '</b>',
+ };
const memoryTo = Number(this.memoryTo);
const memoryFrom = Number(this.memoryFrom);
+ let memoryUsageMsg = '';
if (memoryTo > memoryFrom) {
- return 'increased';
+ memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB'), messageProps, false);
} else if (memoryTo < memoryFrom) {
- return 'decreased';
+ memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB'), messageProps, false);
+ } else {
+ memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB'), messageProps, false);
}
- return 'unchanged';
+ return memoryUsageMsg;
},
},
mounted() {
@@ -130,24 +142,22 @@ export default {
<i
class="fa fa-spinner fa-spin usage-info-load-spinner"
aria-hidden="true">
- </i>Loading deployment statistics
+ </i>{{ s__('mrWidget|Loading deployment statistics') }}
</p>
<p
v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info">
- <a
- :href="metricsMonitoringUrl"
- >Memory</a> usage <b>{{ memoryChangeType }}</b> from {{ memoryFrom }}MB to {{ memoryTo }}MB
+ {{ memoryChangeMessage }}
</p>
<p
v-if="shouldShowLoadFailure"
class="usage-info js-usage-info usage-info-failed">
- Failed to load deployment statistics
+ {{ s__('mrWidget|Failed to load deployment statistics') }}
</p>
<p
v-if="shouldShowMetricsUnavailable"
class="usage-info js-usage-info usage-info-unavailable">
- Deployment statistics are not available currently
+ {{ s__('mrWidget|Deployment statistics are not available currently') }}
</p>
<memory-graph
v-if="shouldShowMemoryGraph"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
deleted file mode 100644
index 142ddf477f1..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import statusIcon from '../mr_widget_status_icon.vue';
-
-export default {
- name: 'MRWidgetSHAMismatch',
- components: {
- statusIcon,
- },
- template: `
- <div class="mr-widget-body media">
- <status-icon status="warning" :show-disabled-button="true" />
- <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
- </span>
- </div>
- </div>
- `,
-};
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
new file mode 100644
index 00000000000..7cc07401911
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
@@ -0,0 +1,25 @@
+<script>
+import statusIcon from '../mr_widget_status_icon.vue';
+
+export default {
+ name: 'ShaMismatch',
+ components: {
+ statusIcon,
+ },
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon
+ status="warning"
+ :show-disabled-button="true"
+ />
+ <div class="media-body space-children">
+ <span class="bold">
+ {{ s__(`mrWidget|The source branch HEAD has recently changed.
+Please reload the page and review the changes before merging`) }}
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 021c2237661..ed15fc6ab0f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -28,7 +28,7 @@ export { default as NothingToMergeState } from './components/states/nothing_to_m
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge';
-export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch';
+export { default as ShaMismatchState } from './components/states/sha_mismatch.vue';
export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 169adfe0a1d..0be5d9e5a55 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -19,7 +19,7 @@ import {
MissingBranchState,
NotAllowedState,
ReadyToMergeState,
- SHAMismatchState,
+ ShaMismatchState,
UnresolvedDiscussionsState,
PipelineBlockedState,
PipelineFailedState,
@@ -227,7 +227,7 @@ export default {
'mr-widget-not-allowed': NotAllowedState,
'mr-widget-missing-branch': MissingBranchState,
'mr-widget-ready-to-merge': ReadyToMergeState,
- 'mr-widget-sha-mismatch': SHAMismatchState,
+ 'mr-widget-sha-mismatch': ShaMismatchState,
'mr-widget-squash-before-merge': SquashBeforeMerge,
'mr-widget-checking': CheckingState,
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 483ad52b8cc..e080ce5c229 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -16,7 +16,7 @@ const stateToComponentMap = {
mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds',
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
- shaMismatch: 'mr-widget-sha-mismatch',
+ shaMismatch: 'sha-mismatch',
rebase: 'mr-widget-rebase',
};
diff --git a/app/assets/javascripts/vue_shared/components/modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
index 5f1364421aa..dcf1489b37c 100644
--- a/app/assets/javascripts/vue_shared/components/modal.vue
+++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/require-default-prop */
export default {
- name: 'Modal',
+ name: 'DeprecatedModal', // use GlModal instead
props: {
id: {
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index c9d7c0f4999..ee1c3498748 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -62,8 +62,7 @@
return `${gon.sprite_file_icons}#${iconName}`;
},
folderIconName() {
- // We don't have a open folder icon yet
- return this.opened ? 'folder' : 'folder';
+ return this.opened ? 'folder-open' : 'folder';
},
iconSizeClass() {
return this.size ? `s${this.size}` : '';
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index c35621c9ef3..21ffdc1dc86 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -1,11 +1,11 @@
<script>
- import modal from './modal.vue';
+ import DeprecatedModal from './deprecated_modal.vue';
export default {
name: 'RecaptchaModal',
components: {
- modal,
+ DeprecatedModal,
},
props: {
@@ -65,7 +65,7 @@
</script>
<template>
- <modal
+ <deprecated-modal
kind="warning"
class="recaptcha-modal js-recaptcha-modal"
:hide-footer="true"
@@ -82,5 +82,5 @@
>
</div>
</div>
- </modal>
+ </deprecated-modal>
</template>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 37d33320445..d0dda50a835 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -446,6 +446,10 @@ img.emoji {
opacity: .5;
}
+.break-word {
+ word-wrap: break-word;
+}
+
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-5 { margin-top: 5px; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 127583626cf..cc74cb72795 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -501,10 +501,8 @@
-moz-osx-font-smoothing: grayscale;
}
- &.dropdown-menu-user-link {
- &::before {
- top: 50%;
- }
+ &.dropdown-menu-user-link::before {
+ top: 50%;
}
}
@@ -624,7 +622,7 @@
}
.dropdown-content {
- max-height: $dropdown-max-height;
+ max-height: 252px;
overflow-y: auto;
}
@@ -701,6 +699,31 @@
border-radius: $border-radius-base;
}
+.git-revision-dropdown {
+ .dropdown-content {
+ max-height: 215px;
+ }
+}
+
+.sidebar-move-issue-dropdown {
+ .dropdown-content {
+ max-height: 160px;
+ }
+}
+
+.dropdown-menu-author {
+ .dropdown-content {
+ max-height: 215px;
+ }
+}
+
+.dropdown-menu-labels {
+ .dropdown-content {
+ max-height: 128px;
+ }
+}
+
+
.dropdown-menu-due-date {
.dropdown-content {
max-height: 230px;
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index db36e27fa74..7f3f7e67d76 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,90 @@
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;
+ }
+}
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 +339,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/header.scss b/app/assets/stylesheets/framework/header.scss
index bea58bade9d..0136af76a13 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -1,60 +1,24 @@
.navbar-gitlab {
- &.navbar-gitlab {
- padding: 0 16px;
- z-index: 1000;
- margin-bottom: 0;
- min-height: $header-height;
- border: 0;
- border-bottom: 1px solid $border-color;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- border-radius: 0;
-
- .logo-text {
- line-height: initial;
-
- svg {
- width: 55px;
- height: 14px;
- margin: 0;
- fill: $white-light;
- }
- }
-
- .container-fluid {
- padding: 0;
-
- .user-counter {
- svg {
- margin-right: 3px;
- }
- }
-
- .navbar-toggle {
- right: -10px;
- border-radius: 0;
- min-width: 45px;
- padding: 0;
- margin-right: -7px;
- font-size: 14px;
- text-align: center;
- color: currentColor;
-
- &:hover,
- &:focus,
- &.active {
- color: currentColor;
- background-color: transparent;
- }
-
- .more-icon,
- .close-icon {
- fill: $white-light;
- margin: auto;
- }
- }
+ padding: 0 16px;
+ z-index: 1000;
+ margin-bottom: 0;
+ min-height: $header-height;
+ border: 0;
+ border-bottom: 1px solid $border-color;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ border-radius: 0;
+
+ .logo-text {
+ line-height: initial;
+
+ svg {
+ width: 55px;
+ height: 14px;
+ margin: 0;
+ fill: $white-light;
}
}
@@ -184,6 +148,38 @@
}
.container-fluid {
+ padding: 0;
+
+ .user-counter {
+ svg {
+ margin-right: 3px;
+ }
+ }
+
+ .navbar-toggle {
+ right: -10px;
+ border-radius: 0;
+ min-width: 45px;
+ padding: 0;
+ margin-right: -7px;
+ font-size: 14px;
+ text-align: center;
+ color: currentColor;
+
+ &:hover,
+ &:focus,
+ &.active {
+ color: currentColor;
+ background-color: transparent;
+ }
+
+ .more-icon,
+ .close-icon {
+ fill: $white-light;
+ margin: auto;
+ }
+ }
+
.navbar-nav {
@media (max-width: $screen-xs-max) {
display: -webkit-flex;
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 2d015ef086b..df1cafc9f8e 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -20,7 +20,7 @@
width: 100%;
}
- $image-widths: 250 306 394 430;
+ $image-widths: 80 250 306 394 430;
@each $width in $image-widths {
&.svg-#{$width} {
img,
@@ -39,12 +39,35 @@
svg {
fill: currentColor;
- &.s8 { @include svg-size(8px); }
- &.s12 { @include svg-size(12px); }
- &.s16 { @include svg-size(16px); }
- &.s18 { @include svg-size(18px); }
- &.s24 { @include svg-size(24px); }
- &.s32 { @include svg-size(32px); }
- &.s48 { @include svg-size(48px); }
- &.s72 { @include svg-size(72px); }
+ &.s8 {
+ @include svg-size(8px);
+ }
+
+ &.s12 {
+ @include svg-size(12px);
+ }
+
+ &.s16 {
+ @include svg-size(16px);
+ }
+
+ &.s18 {
+ @include svg-size(18px);
+ }
+
+ &.s24 {
+ @include svg-size(24px);
+ }
+
+ &.s32 {
+ @include svg-size(32px);
+ }
+
+ &.s48 {
+ @include svg-size(48px);
+ }
+
+ &.s72 {
+ @include svg-size(72px);
+ }
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index d1d98270ad9..3dd4a613789 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -152,3 +152,4 @@
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
+
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index c03d4c2eebf..318d3ddaece 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -31,8 +31,12 @@
.dropdown-menu-issues-board-new {
width: 320px;
+ .open & {
+ max-height: 400px;
+ }
+
.dropdown-content {
- max-height: 150px;
+ max-height: 162px;
}
}
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss
index 3e2fa8ca88d..49fe50977f5 100644
--- a/app/assets/stylesheets/pages/branches.scss
+++ b/app/assets/stylesheets/pages/branches.scss
@@ -1,6 +1,17 @@
+.content-list > .branch-item,
+.branch-title {
+ display: flex;
+ align-items: center;
+}
+
+.branch-info {
+ flex: auto;
+ min-width: 0;
+ overflow: hidden;
+}
+
.divergence-graph {
- padding: 12px 12px 0 0;
- float: right;
+ padding: 0 6px;
.graph-side {
position: relative;
@@ -53,3 +64,9 @@
background-color: $divergence-graph-separator-bg;
}
}
+
+.divergence-graph,
+.branch-item .controls {
+ flex: 0 0 auto;
+ white-space: nowrap;
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 8871a069d5d..d9267f5cdf3 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -162,17 +162,14 @@
* Last push widget
*/
.event-last-push {
- overflow: auto;
width: 100%;
+ display: flex;
+ align-items: center;
.event-last-push-text {
@include str-truncated(100%);
- padding: 4px 0;
font-size: 13px;
- float: left;
- margin-right: -150px;
- padding-right: 150px;
- line-height: 20px;
+ margin-right: $gl-padding;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 0f49d15203b..b0852adb459 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -26,9 +26,15 @@
}
}
+.dropdown-menu-labels {
+ .dropdown-content {
+ max-height: 135px;
+ }
+}
+
.dropdown-new-label {
.dropdown-content {
- max-height: 260px;
+ max-height: 136px;
}
}
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/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 085a2e74328..81e98f358a8 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -140,12 +140,6 @@ ul.notes {
@include bulleted-list;
word-wrap: break-word;
- ul.task-list {
- ul:not(.task-list) {
- padding-left: 1.3em;
- }
- }
-
table {
@include markdown-table;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 85de0d8e70f..9a770d77685 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -9,7 +9,6 @@
.new_project,
.edit-project,
.import-project {
-
.help-block {
margin-bottom: 10px;
}
@@ -18,18 +17,25 @@
border-radius: $border-radius-base;
}
- .input-group > div {
+ .input-group {
+ display: flex;
- &:last-child {
- padding-right: 0;
+ .select2-container {
+ display: unset;
+ max-width: unset;
+ width: unset !important;
+ flex-grow: 1;
+ }
+
+ > div {
+ &:last-child {
+ padding-right: 0;
+ }
}
}
@media (max-width: $screen-xs-max) {
.input-group > div {
-
- margin-bottom: 14px;
-
&:last-child {
margin-bottom: 0;
}
@@ -41,17 +47,24 @@
}
.input-group-addon {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: unset;
+ width: unset;
+ max-width: 50%;
+ text-align: left;
&.static-namespace {
height: 35px;
border-radius: 3px;
border: 1px solid $border-color;
+ max-width: 100%;
+ flex-grow: 1;
}
+ .select2 a,
+ .btn-default {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
+ border-radius: 0 $border-radius-base $border-radius-base 0;
}
}
}
@@ -290,7 +303,7 @@
font-size: 13px;
font-weight: $gl-font-weight-bold;
line-height: 13px;
- letter-spacing: .4px;
+ letter-spacing: 0.4px;
padding: 6px 14px;
text-align: center;
vertical-align: middle;
@@ -443,7 +456,7 @@ a.deploy-project-label {
text-decoration: none;
&.disabled {
- opacity: .3;
+ opacity: 0.3;
cursor: not-allowed;
}
}
@@ -600,26 +613,26 @@ a.deploy-project-label {
}
.first-column {
- @media(min-width: $screen-xs-min) {
+ @media (min-width: $screen-xs-min) {
max-width: 50%;
padding-right: 30px;
}
- @media(max-width: $screen-xs-max) {
+ @media (max-width: $screen-xs-max) {
max-width: 100%;
width: 100%;
}
}
.second-column {
- @media(min-width: $screen-xs-min) {
+ @media (min-width: $screen-xs-min) {
width: 50%;
flex: 1;
padding-left: 30px;
position: relative;
}
- @media(max-width: $screen-xs-max) {
+ @media (max-width: $screen-xs-max) {
max-width: 100%;
width: 100%;
padding-left: 0;
@@ -632,7 +645,7 @@ a.deploy-project-label {
}
&::before {
- content: "OR";
+ content: 'OR';
position: absolute;
left: -10px;
top: 50%;
@@ -656,7 +669,7 @@ a.deploy-project-label {
}
&::after {
- content: "";
+ content: '';
position: absolute;
background-color: $border-color;
bottom: 0;
@@ -921,10 +934,7 @@ pre.light-well {
border-right: solid 1px transparent;
}
}
-}
-.protected-tags-list,
-.protected-branches-list {
.dropdown-menu-toggle {
width: 100%;
max-width: 300px;
@@ -1111,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 8265b8370f7..65046f6665e 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -19,6 +19,7 @@
.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;
@@ -28,6 +29,11 @@
max-width: 250px;
}
}
+
+ .file-status-icon {
+ width: 10px;
+ height: 10px;
+ }
}
.ide-file-list {
@@ -40,31 +46,41 @@
background: $white-normal;
}
- .repo-file-name {
+ .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;
+ }
}
- .unsaved-icon {
- color: $indigo-700;
- float: right;
- font-size: smaller;
- line-height: 20px;
+ .ide-file-changed-icon {
+ margin-left: auto;
}
- .repo-new-btn {
+ .ide-new-btn {
display: none;
- margin-top: -4px;
margin-bottom: -4px;
+ margin-right: -8px;
}
&:hover {
- .repo-new-btn {
+ .ide-new-btn {
display: block;
}
+ }
- .unsaved-icon {
- display: none;
+ &.folder {
+ svg {
+ fill: $gl-text-color-secondary;
}
}
}
@@ -79,10 +95,10 @@
}
}
-.multi-file-table-name,
-.multi-file-table-col-commit-message {
+.file-name,
+.file-col-commit-message {
+ display: flex;
overflow: visible;
- max-width: 0;
padding: 6px 12px;
}
@@ -99,21 +115,6 @@
}
}
-table.table tr td.multi-file-table-name {
- width: 350px;
- padding: 6px 12px;
-
- svg {
- vertical-align: middle;
- margin-right: 2px;
- }
-
- .loading-container {
- margin-right: 4px;
- display: inline-block;
- }
-}
-
.multi-file-table-col-commit-message {
white-space: nowrap;
width: 50%;
@@ -129,13 +130,35 @@ table.table tr td.multi-file-table-name {
.multi-file-tabs {
display: flex;
- overflow-x: auto;
background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark;
- > li {
+ > 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 {
@@ -160,20 +183,32 @@ table.table tr td.multi-file-table-name {
position: absolute;
right: 8px;
top: 50%;
+ width: 16px;
+ height: 16px;
padding: 0;
background: none;
border: 0;
- font-size: $gl-font-size;
- color: $gray-darkest;
+ border-radius: $border-radius-default;
+ color: $theme-gray-900;
transform: translateY(-50%);
- &:not(.modified):hover,
- &:not(.modified):focus {
- color: $hint-color;
+ svg {
+ position: relative;
+ top: -1px;
}
- &.modified {
- color: $indigo-700;
+ &:hover {
+ background-color: $theme-gray-200;
+ }
+
+ &:focus {
+ background-color: $blue-500;
+ color: $white-light;
+ outline: 0;
+
+ svg {
+ fill: currentColor;
+ }
}
}
@@ -192,6 +227,74 @@ table.table tr td.multi-file-table-name {
.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 {
@@ -252,7 +355,7 @@ table.table tr td.multi-file-table-name {
display: flex;
position: relative;
flex-direction: column;
- width: 290px;
+ width: 340px;
padding: 0;
background-color: $gray-light;
padding-right: 3px;
@@ -299,7 +402,7 @@ table.table tr td.multi-file-table-name {
}
.branch-container {
- border-left: 4px solid $indigo-700;
+ border-left: 4px solid;
margin-bottom: $gl-bar-padding;
}
@@ -311,7 +414,6 @@ table.table tr td.multi-file-table-name {
.branch-header-title {
flex: 1;
padding: $grid-size $gl-padding;
- color: $indigo-700;
font-weight: $gl-font-weight-bold;
svg {
@@ -350,6 +452,11 @@ table.table tr td.multi-file-table-name {
flex: 1;
}
+.multi-file-commit-empty-state-container {
+ align-items: center;
+ justify-content: center;
+}
+
.multi-file-commit-panel-header {
display: flex;
align-items: center;
@@ -376,7 +483,7 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-panel-header-title {
display: flex;
flex: 1;
- padding: $gl-btn-padding;
+ padding: 0 $gl-btn-padding;
svg {
margin-right: $gl-btn-padding;
@@ -390,12 +497,34 @@ table.table tr td.multi-file-table-name {
.multi-file-commit-list {
flex: 1;
overflow: auto;
- padding: $gl-padding;
+ 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 {
@@ -414,29 +543,58 @@ table.table tr td.multi-file-table-name {
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;
-}
-
-.multi-file-commit-fieldset {
- display: flex;
- align-items: center;
- padding-bottom: 12px;
.btn {
- flex: 1;
+ font-size: $gl-font-size;
}
}
.multi-file-commit-message.form-control {
- height: 80px;
+ height: 160px;
resize: none;
}
@@ -468,7 +626,7 @@ table.table tr td.multi-file-table-name {
top: 0;
width: 100px;
height: 1px;
- background-color: rgba($red-500, .5);
+ background-color: rgba($red-500, 0.5);
}
}
}
@@ -487,7 +645,7 @@ table.table tr td.multi-file-table-name {
justify-content: center;
}
-.repo-new-btn {
+.ide-new-btn {
.dropdown-toggle svg {
margin-top: -2px;
margin-bottom: 2px;
@@ -505,36 +663,39 @@ table.table tr td.multi-file-table-name {
}
}
-.ide.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;
- }
+.ide {
+ overflow: hidden;
- .content {
- margin-top: $header-height;
- }
+ &.nav-only {
+ .flash-container {
+ margin-top: $header-height;
+ margin-bottom: 0;
+ }
- .multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
- max-height: calc(100vh - #{$header-height + $context-header-height});
- }
+ .alert-wrapper .flash-container .flash-alert:last-child,
+ .alert-wrapper .flash-container .flash-notice:last-child {
+ margin-bottom: 0;
+ }
- &.flash-shown {
- .content {
- margin-top: 0;
+ .content-wrapper {
+ margin-top: $header-height;
+ padding-bottom: 0;
}
- .ide-view {
- height: calc(100vh - #{$header-height + $flash-height});
+ &.flash-shown {
+ .content-wrapper {
+ margin-top: 0;
+ }
+
+ .ide-view {
+ height: calc(100vh - #{$header-height + $flash-height});
+ }
}
- .multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
- max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height});
+ .projects-sidebar {
+ .multi-file-commit-panel-inner-scroll {
+ flex: 1;
+ }
}
}
}
@@ -544,34 +705,28 @@ table.table tr td.multi-file-table-name {
margin-top: #{$header-height + $performance-bar-height};
}
- .content {
+ .content-wrapper {
margin-top: #{$header-height + $performance-bar-height};
+ padding-bottom: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height});
}
- .multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
- max-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
- }
-
&.flash-shown {
- .content {
+ .content-wrapper {
margin-top: 0;
}
.ide-view {
- height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
- }
-
- .multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
- max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
+ height: calc(
+ 100vh - #{$header-height + $performance-bar-height + $flash-height}
+ );
}
}
}
-
.dragHandle {
position: absolute;
top: 0;
@@ -587,3 +742,31 @@ table.table tr td.multi-file-table-name {
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;
+ display: flex;
+ align-items: center;
+ font-weight: $gl-font-weight-bold;
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index c9363188505..dbde0720993 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -112,7 +112,7 @@ input[type="checkbox"]:hover {
}
.dropdown-content {
- max-height: 350px;
+ max-height: 302px;
}
}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 6e539e39ca1..45ae94abaff 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -1,8 +1,8 @@
-@import "framework/variables";
-@import "peek/views/performance_bar";
-@import "peek/views/rblineprof";
+@import 'framework/variables';
+@import 'peek/views/performance_bar';
+@import 'peek/views/rblineprof';
-#peek {
+#js-peek {
position: fixed;
left: 0;
top: 0;
@@ -15,26 +15,36 @@
line-height: $performance-bar-height;
color: $perf-bar-text;
+ select {
+ width: 200px;
+ }
+
&.disabled {
display: none;
}
&.production {
background-color: $perf-bar-production;
+
+ select {
+ background: $perf-bar-production;
+ }
}
&.staging {
background-color: $perf-bar-staging;
+
+ select {
+ background: $perf-bar-staging;
+ }
}
&.development {
background-color: $perf-bar-development;
- }
- .wrapper {
- width: 80%;
- height: $performance-bar-height;
- margin: 0 auto;
+ select {
+ background: $perf-bar-development;
+ }
}
// UI Elements
@@ -42,11 +52,12 @@
background: $perf-bar-bucket-bg;
display: inline-block;
padding: 4px 6px;
- font-family: Consolas, "Liberation Mono", Courier, monospace;
+ font-family: Consolas, 'Liberation Mono', Courier, monospace;
line-height: 1;
color: $perf-bar-bucket-color;
border-radius: 3px;
- box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, inset 0 1px 2px $perf-bar-bucket-box-shadow-to;
+ box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from,
+ inset 0 1px 2px $perf-bar-bucket-box-shadow-to;
.hidden {
display: none;
@@ -94,6 +105,16 @@
max-width: 10000px !important;
}
}
+
+ .performance-bar-modal {
+ .modal-footer {
+ display: none;
+ }
+
+ .modal-dialog {
+ width: 860px;
+ }
+ }
}
#modal-peek-pg-queries-content {
diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb
index c27f2ee3c09..a4648b33cfa 100644
--- a/app/controllers/admin/application_controller.rb
+++ b/app/controllers/admin/application_controller.rb
@@ -3,23 +3,9 @@
# Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController
before_action :authenticate_admin!
- before_action :display_read_only_information
layout 'admin'
def authenticate_admin!
render_404 unless current_user.admin?
end
-
- def display_read_only_information
- return unless Gitlab::Database.read_only?
-
- flash.now[:notice] = read_only_message
- end
-
- private
-
- # Overridden in EE
- def read_only_message
- _('You are on a read-only GitLab instance.')
- end
end
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/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/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/ide_controller.rb b/app/controllers/ide_controller.rb
new file mode 100644
index 00000000000..1ff25a45398
--- /dev/null
+++ b/app/controllers/ide_controller.rb
@@ -0,0 +1,6 @@
+class IdeController < ApplicationController
+ layout 'nav_only'
+
+ def index
+ end
+end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 8440945ab43..5e6676ea513 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -18,6 +18,18 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
+ # Extend the standard implementation to also increment
+ # the number of failed sign in attempts
+ def failure
+ if params[:username].present? && AuthHelper.form_based_provider?(failed_strategy.name)
+ user = User.by_login(params[:username])
+
+ user&.increment_failed_attempts!
+ end
+
+ super
+ end
+
# Extend the standard message generation to accept our custom exception
def failure_message
exception = env["omniauth.error"]
@@ -95,6 +107,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
handle_omniauth
end
+ def auth0
+ if oauth['uid'].blank?
+ fail_auth0_login
+ else
+ handle_omniauth
+ end
+ end
+
private
def handle_omniauth
@@ -170,6 +190,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to new_user_session_path
end
+ def fail_auth0_login
+ flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
+
+ redirect_to new_user_session_path
+ end
+
def handle_disabled_provider
label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
flash[:alert] = "Signing in using #{label} has been disabled"
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/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/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/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..cf84629fadc 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -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
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index d421b1a8eb5..cae6e2c40b8 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -21,4 +21,26 @@ class Projects::PagesController < Projects::ApplicationController
end
end
end
+
+ def update
+ result = Projects::UpdateService.new(@project, current_user, project_params).execute
+
+ respond_to do |format|
+ format.html do
+ if result[:status] == :success
+ flash[:notice] = 'Your changes have been saved'
+ else
+ flash[:alert] = 'Something went wrong on our end'
+ end
+
+ redirect_to project_pages_path(@project)
+ end
+ end
+ end
+
+ private
+
+ def project_params
+ params.require(:project).permit(:pages_https_only)
+ end
end
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 06ce7328fb5..557671ab186 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -10,10 +10,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
if service.execute
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
- if service.run_auto_devops_pipeline?
- 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
+ run_autodevops_pipeline(service)
redirect_to project_settings_ci_cd_path(@project)
else
@@ -24,6 +21,18 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
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,
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/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 259809f3429..96125b549b7 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -29,12 +29,12 @@ module Projects
@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 +42,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/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/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 5c507fe8d50..2c8f21c2400 100644
--- a/app/finders/admin/projects_finder.rb
+++ b/app/finders/admin/projects_finder.rb
@@ -16,6 +16,7 @@ class Admin::ProjectsFinder
items = by_archived(items)
items = by_personal(items)
items = by_name(items)
+ items = items.includes(namespace: [:owner])
sort(items).page(params[:page])
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index af9c8bf1bd3..701be97ee96 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -300,7 +300,7 @@ module ApplicationHelper
def linkedin_url(user)
name = user.linkedin
- if name =~ %r{\Ahttps?:\/\/(www\.)?linkedin\.com\/in\/}
+ if name =~ %r{\Ahttps?://(www\.)?linkedin\.com/in/}
name
else
"https://www.linkedin.com/in/#{name}"
@@ -309,10 +309,10 @@ module ApplicationHelper
def twitter_url(user)
name = user.twitter
- if name =~ %r{\Ahttps?:\/\/(www\.)?twitter\.com\/}
+ if name =~ %r{\Ahttps?://(www\.)?twitter\.com/}
name
else
- "https://www.twitter.com/#{name}"
+ "https://twitter.com/#{name}"
end
end
@@ -323,4 +323,11 @@ module ApplicationHelper
def locale_path
asset_path("locale/#{Gitlab::I18n.locale}/app.js")
end
+
+ # Overridden in EE
+ def read_only_message
+ return unless Gitlab::Database.read_only?
+
+ _('You are on a read-only GitLab instance.')
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 4c4d7cca8a5..b3b080e6dcf 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -96,7 +96,7 @@ module ApplicationSettingsHelper
def repository_storages_options_for_select(selected)
options = Gitlab.config.repositories.storages.map do |name, storage|
- ["#{name} - #{storage['path']}", name]
+ ["#{name} - #{storage['gitaly_address']}", name]
end
options_for_select(options, selected)
@@ -245,7 +245,8 @@ module ApplicationSettingsHelper
:usage_ping_enabled,
:user_default_external,
:user_oauth_applications,
- :version_check_enabled
+ :version_check_enabled,
+ :allow_local_requests_from_hooks_and_services
]
end
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 5ff09b23a78..2b440e4d584 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -33,6 +33,17 @@ module BlobHelper
ref)
end
+ def ide_edit_button(project = @project, ref = @ref, path = @path, options = {})
+ return unless blob = readable_blob(options, path, project, ref)
+
+ edit_button_tag(blob,
+ 'btn btn-default',
+ _('Web IDE'),
+ ide_edit_path(project, ref, path, options),
+ project,
+ ref)
+ end
+
def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index f6ddb6d4cfe..6d6b840f485 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -377,4 +377,11 @@ module IssuablesHelper
def parent
@project || @group
end
+
+ def issuable_milestone_tooltip_title(issuable)
+ if issuable.milestone
+ milestone_tooltip = milestone_tooltip_title(issuable.milestone)
+ _('Milestone') + (milestone_tooltip ? ': ' + milestone_tooltip : '')
+ end
+ end
end
diff --git a/app/helpers/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/projects_helper.rb b/app/helpers/projects_helper.rb
index da9fe734f1c..15f48e43a28 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -531,4 +531,22 @@ module ProjectsHelper
def can_show_last_commit_in_list?(project)
can?(current_user, :read_cross_project) && project.commit
end
+
+ def pages_https_only_disabled?
+ !@project.pages_domains.all?(&:https?)
+ end
+
+ def pages_https_only_title
+ return unless pages_https_only_disabled?
+
+ "You must enable HTTPS for all your domains first"
+ end
+
+ def pages_https_only_label_class
+ if pages_https_only_disabled?
+ "list-label disabled"
+ else
+ "list-label"
+ end
+ end
end
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 240783bc7fd..f435c80c656 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -1,27 +1,4 @@
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 "issue", "issue_events"
- "Event will be triggered when an issue is created/updated/closed"
- when "confidential_issue", "confidential_issue_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/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 5fe09cea83f..be99f3780cc 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -11,6 +11,14 @@ 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
+
+ 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/application_setting.rb b/app/models/application_setting.rb
index 3cbbf8b5dfa..862933bf127 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -330,7 +330,8 @@ class ApplicationSetting < ActiveRecord::Base
usage_ping_enabled: Settings.gitlab['usage_ping_enabled'],
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
- gitaly_timeout_default: 55
+ gitaly_timeout_default: 55,
+ allow_local_requests_from_hooks_and_services: false
}
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index c1da2081465..08bb5915d10 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -3,6 +3,7 @@ module Ci
prepend ArtifactMigratable
include TokenAuthenticatable
include AfterCommitQueue
+ include ObjectStorage::BackgroundMove
include Presentable
include Importable
@@ -45,6 +46,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) }
@@ -140,7 +142,11 @@ module Ci
next if build.retries_max.zero?
if build.retries_count < build.retries_max
- Ci::Build.retry(build, build.user)
+ begin
+ Ci::Build.retry(build, build.user)
+ rescue Gitlab::Access::AccessDeniedError => ex
+ Rails.logger.error "Unable to auto-retry job #{build.id}: #{ex}"
+ end
end
end
@@ -328,8 +334,7 @@ module Ci
end
def erase_old_trace!
- write_attribute(:trace, nil)
- save
+ update_column(:trace, nil)
end
def needs_touch?
@@ -362,13 +367,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!
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 f2edcdd61fd..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
@@ -14,7 +15,7 @@ module Ci
has_many :stages
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
- has_many :builds, foreign_key: :commit_id
+ has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
@@ -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/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..bfdfc5ae6fe 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,7 @@ module Clusters
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
+ scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
def status_name
if provider
diff --git a/app/models/commit.rb b/app/models/commit.rb
index cceae5efb72..b64462fb768 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -175,7 +175,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/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb
new file mode 100644
index 00000000000..4b66725a3e6
--- /dev/null
+++ b/app/models/concerns/atomic_internal_id.rb
@@ -0,0 +1,46 @@
+# Include atomic internal id generation scheme for a model
+#
+# This allows us to atomically generate internal ids that are
+# unique within a given scope.
+#
+# For example, let's generate internal ids for Issue per Project:
+# ```
+# class Issue < ActiveRecord::Base
+# has_internal_id :iid, scope: :project, init: ->(s) { s.project.issues.maximum(:iid) }
+# end
+# ```
+#
+# This generates unique internal ids per project for newly created issues.
+# The generated internal id is saved in the `iid` attribute of `Issue`.
+#
+# This concern uses InternalId records to facilitate atomicity.
+# In the absence of a record for the given scope, one will be created automatically.
+# In this situation, the `init` block is called to calculate the initial value.
+# In the example above, we calculate the maximum `iid` of all issues
+# within the given project.
+#
+# Note that a model may have more than one internal id associated with possibly
+# different scopes.
+module AtomicInternalId
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName
+ before_validation(on: :create) do
+ if read_attribute(column).blank?
+ scope_attrs = { scope => association(scope).reader }
+ usage = self.class.table_name.to_sym
+
+ new_iid = InternalId.generate_next(self, scope_attrs, usage, init)
+ write_attribute(column, new_iid)
+ end
+ end
+
+ validates column, presence: true, numericality: true
+ end
+ end
+
+ def to_param
+ iid.to_s
+ end
+end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index d35e37935fb..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 }
@@ -21,7 +22,7 @@ module Avatarable
def avatar_type
unless self.avatar.image?
- self.errors.add :avatar, "only images allowed"
+ errors.add :avatar, "file format is not supported. Please try one of the following supported formats: #{AvatarUploader::IMAGE_EXT.join(', ')}"
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/internal_id.rb b/app/models/concerns/nonatomic_internal_id.rb
index 01079fb8bd6..9d0c9b8512f 100644
--- a/app/models/concerns/internal_id.rb
+++ b/app/models/concerns/nonatomic_internal_id.rb
@@ -1,4 +1,4 @@
-module InternalId
+module NonatomicInternalId
extend ActiveSupport::Concern
included do
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 66e61c06765..e18ea8bfea4 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -1,5 +1,5 @@
class Deployment < ActiveRecord::Base
- include InternalId
+ include NonatomicInternalId
belongs_to :project, required: true
belongs_to :environment, required: true
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 8d183006c65..d99af79b5fe 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -189,12 +189,6 @@ class Group < Namespace
owners.include?(user) && owners.size == 1
end
- def avatar_type
- unless self.avatar.image?
- self.errors.add :avatar, "only images allowed"
- end
- end
-
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
@@ -230,13 +224,13 @@ class Group < Namespace
end
GroupMember
- .active_without_invites
+ .active_without_invites_and_requests
.where(source_id: source_ids)
end
def members_with_descendants
GroupMember
- .active_without_invites
+ .active_without_invites_and_requests
.where(source_id: self_and_descendants.reorder(nil).select(:id))
end
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
new file mode 100644
index 00000000000..cbec735c2dd
--- /dev/null
+++ b/app/models/internal_id.rb
@@ -0,0 +1,125 @@
+# An InternalId is a strictly monotone sequence of integers
+# generated for a given scope and usage.
+#
+# For example, issues use their project to scope internal ids:
+# In that sense, scope is "project" and usage is "issues".
+# Generated internal ids for an issue are unique per project.
+#
+# See InternalId#usage enum for available usages.
+#
+# In order to leverage InternalId for other usages, the idea is to
+# * Add `usage` value to enum
+# * (Optionally) add columns to `internal_ids` if needed for scope.
+class InternalId < ActiveRecord::Base
+ belongs_to :project
+
+ enum usage: { issues: 0 }
+
+ validates :usage, presence: true
+
+ REQUIRED_SCHEMA_VERSION = 20180305095250
+
+ # Increments #last_value and saves the record
+ #
+ # The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
+ # As such, the increment is atomic and safe to be called concurrently.
+ def increment_and_save!
+ lock!
+ self.last_value = (last_value || 0) + 1
+ save!
+ last_value
+ end
+
+ class << self
+ def generate_next(subject, scope, usage, init)
+ # Shortcut if `internal_ids` table is not available (yet)
+ # This can be the case in other (unrelated) migration specs
+ return (init.call(subject) || 0) + 1 unless available?
+
+ InternalIdGenerator.new(subject, scope, usage, init).generate
+ end
+
+ def available?
+ @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization
+ end
+
+ # Flushes cached information about schema
+ def reset_column_information
+ @available_flag = nil
+ super
+ end
+ end
+
+ class InternalIdGenerator
+ # Generate next internal id for a given scope and usage.
+ #
+ # For currently supported usages, see #usage enum.
+ #
+ # The method implements a locking scheme that has the following properties:
+ # 1) Generated sequence of internal ids is unique per (scope and usage)
+ # 2) The method is thread-safe and may be used in concurrent threads/processes.
+ # 3) The generated sequence is gapless.
+ # 4) In the absence of a record in the internal_ids table, one will be created
+ # and last_value will be calculated on the fly.
+ #
+ # subject: The instance we're generating an internal id for. Gets passed to init if called.
+ # scope: Attributes that define the scope for id generation.
+ # usage: Symbol to define the usage of the internal id, see InternalId.usages
+ # init: Block that gets called to initialize InternalId record if not present
+ # Make sure to not throw exceptions in the absence of records (if this is expected).
+ attr_reader :subject, :scope, :init, :scope_attrs, :usage
+
+ def initialize(subject, scope, usage, init)
+ @subject = subject
+ @scope = scope
+ @init = init
+ @usage = usage
+
+ raise ArgumentError, 'Scope is not well-defined, need at least one column for scope (given: 0)' if scope.empty?
+
+ unless InternalId.usages.has_key?(usage.to_s)
+ raise ArgumentError, "Usage '#{usage}' is unknown. Supported values are #{InternalId.usages.keys} from InternalId.usages"
+ end
+ end
+
+ # Generates next internal id and returns it
+ def generate
+ subject.transaction do
+ # Create a record in internal_ids if one does not yet exist
+ # and increment its last value
+ #
+ # Note this will acquire a ROW SHARE lock on the InternalId record
+ (lookup || create_record).increment_and_save!
+ end
+ end
+
+ private
+
+ # Retrieve InternalId record for (project, usage) combination, if it exists
+ def lookup
+ InternalId.find_by(**scope, usage: usage_value)
+ end
+
+ def usage_value
+ @usage_value ||= InternalId.usages[usage.to_s]
+ end
+
+ # Create InternalId record for (scope, usage) combination, if it doesn't exist
+ #
+ # We blindly insert without synchronization. If another process
+ # was faster in doing this, we'll realize once we hit the unique key constraint
+ # violation. We can safely roll-back the nested transaction and perform
+ # a lookup instead to retrieve the record.
+ def create_record
+ subject.transaction(requires_new: true) do
+ InternalId.create!(
+ **scope,
+ usage: usage_value,
+ last_value: init.call(subject) || 0
+ )
+ end
+ rescue ActiveRecord::RecordNotUnique
+ lookup
+ end
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index c81f7e52bb1..7bfc45c1f43 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -1,7 +1,7 @@
require 'carrierwave/orm/activerecord'
class Issue < ActiveRecord::Base
- include InternalId
+ include AtomicInternalId
include Issuable
include Noteable
include Referable
@@ -24,6 +24,8 @@ class Issue < ActiveRecord::Base
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
+ has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
+
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_requests_closing_issues,
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 ec8156bbb01..e1a32148538 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -52,7 +52,7 @@ class Member < ActiveRecord::Base
end
# Like active, but without invites. For when a User is required.
- scope :active_without_invites, -> do
+ scope :active_without_invites_and_requests, -> do
left_join_users
.where(users: { state: 'active' })
.non_request
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 149ef7ec429..91d8be5559b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1,5 +1,5 @@
class MergeRequest < ActiveRecord::Base
- include InternalId
+ include NonatomicInternalId
include Issuable
include Noteable
include Referable
@@ -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 77c19380e66..e7d397f40f5 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -8,7 +8,7 @@ class Milestone < ActiveRecord::Base
Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
- include InternalId
+ include NonatomicInternalId
include Sortable
include Referable
include StripAttribute
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index fd70e920c7e..b3ffad00a07 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -35,7 +35,8 @@ class NotificationRecipient
# check this last because it's expensive
# nobody should receive notifications if they've specifically unsubscribed
- return false if unsubscribed?
+ # except if they were mentioned.
+ return false if @type != :mention && unsubscribed?
true
end
@@ -47,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
@@ -95,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/pages_domain.rb b/app/models/pages_domain.rb
index 588bd50ed77..2e478a24778 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -6,8 +6,10 @@ class PagesDomain < ActiveRecord::Base
validates :domain, hostname: { allow_numeric_hostname: true }
validates :domain, uniqueness: { case_sensitive: false }
- validates :certificate, certificate: true, allow_nil: true, allow_blank: true
- validates :key, certificate_key: true, allow_nil: true, allow_blank: true
+ validates :certificate, presence: { message: 'must be present if HTTPS-only is enabled' }, if: ->(domain) { domain.project&.pages_https_only? }
+ validates :certificate, certificate: true, if: ->(domain) { domain.certificate.present? }
+ validates :key, presence: { message: 'must be present if HTTPS-only is enabled' }, if: ->(domain) { domain.project&.pages_https_only? }
+ validates :key, certificate_key: true, if: ->(domain) { domain.key.present? }
validates :verification_code, presence: true, allow_blank: false
validate :validate_pages_domain
@@ -46,6 +48,10 @@ class PagesDomain < ActiveRecord::Base
!Gitlab::CurrentSettings.pages_domain_verification_enabled? || enabled_until.present?
end
+ def https?
+ certificate.present?
+ end
+
def to_param
domain
end
diff --git a/app/models/project.rb b/app/models/project.rb
index d6e663f14e4..6a420663644 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -38,6 +38,9 @@ class Project < ActiveRecord::Base
attachments: 2
}.freeze
+ # Valids ports to import from
+ VALID_IMPORT_PORTS = [22, 80, 443].freeze
+
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
@@ -188,6 +191,8 @@ class Project < ActiveRecord::Base
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :internal_ids
+
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
@@ -262,6 +267,7 @@ class Project < ActiveRecord::Base
validate :visibility_level_allowed_by_group
validate :visibility_level_allowed_as_fork
validate :check_wiki_path_conflict
+ validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
validates :repository_storage,
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
@@ -498,7 +504,7 @@ class Project < ActiveRecord::Base
end
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
+ Gitlab.config.repositories.storages[repository_storage]&.legacy_disk_path
end
def team
@@ -732,6 +738,26 @@ class Project < ActiveRecord::Base
end
end
+ def pages_https_only
+ return false unless Gitlab.config.pages.external_https
+
+ super
+ end
+
+ def pages_https_only?
+ return false unless Gitlab.config.pages.external_https
+
+ super
+ end
+
+ def validate_pages_https_only
+ return unless pages_https_only?
+
+ unless pages_domains.all?(&:https?)
+ errors.add(:pages_https_only, "cannot be enabled unless all domains have TLS certificates")
+ end
+ end
+
def to_param
if persisted? && errors.include?(:path)
path_was
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index ae6af732ed4..4234b8044e5 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -1,6 +1,4 @@
class AssemblaService < Service
- include HTTParty
-
prop_accessor :token, :subdomain
validates :token, presence: true, if: :activated?
@@ -31,6 +29,6 @@ class AssemblaService < Service
return unless supported_events.include?(data[:object_kind])
url = "https://atlas.assembla.com/spaces/#{subdomain}/github_tool?secret_key=#{token}"
- AssemblaService.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
+ Gitlab::HTTP.post(url, body: { payload: data }.to_json, headers: { 'Content-Type' => 'application/json' })
end
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 42939ea0ec8..54e4b3278db 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -117,14 +117,14 @@ class BambooService < CiService
url = build_url(path)
if username.blank? && password.blank?
- HTTParty.get(url, verify: false)
+ Gitlab::HTTP.get(url, verify: false)
else
url << '&os_authType=basic'
- HTTParty.get(url, verify: false,
- basic_auth: {
- username: username,
- password: password
- })
+ Gitlab::HTTP.get(url, verify: false,
+ basic_auth: {
+ username: username,
+ password: password
+ })
end
end
end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index fc30f6e3365..d2aaff8817a 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -71,7 +71,7 @@ class BuildkiteService < CiService
end
def calculate_reactive_cache(sha, ref)
- response = HTTParty.get(commit_status_path(sha), verify: false)
+ response = Gitlab::HTTP.get(commit_status_path(sha), verify: false)
status =
if response.code == 200 && response['status']
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 8d7a4fceb08..cb4af73807b 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -1,6 +1,4 @@
class CampfireService < Service
- include HTTParty
-
prop_accessor :token, :subdomain, :room
validates :token, presence: true, if: :activated?
@@ -31,7 +29,6 @@ class CampfireService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
- self.class.base_uri base_uri
message = build_message(data)
speak(self.room, message, auth)
end
@@ -69,14 +66,14 @@ class CampfireService < Service
}
}
}
- res = self.class.post(path, auth.merge(body))
+ res = Gitlab::HTTP.post(path, base_uri: base_uri, **auth.merge(body))
res.code == 201 ? res : nil
end
# Returns a list of rooms, or [].
# https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
def rooms(auth)
- res = self.class.get("/rooms.json", auth)
+ res = Gitlab::HTTP.get("/rooms.json", base_uri: base_uri, **auth)
res.code == 200 ? res["rooms"] : []
end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index c93f1632652..71b10fc6bc1 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -49,7 +49,7 @@ class DroneCiService < CiService
end
def calculate_reactive_cache(sha, ref)
- response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
+ response = Gitlab::HTTP.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
status =
if response.code == 200 && response['status']
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index 720ad61162e..1553f169827 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -1,6 +1,4 @@
class ExternalWikiService < Service
- include HTTParty
-
prop_accessor :external_wiki_url
validates :external_wiki_url, presence: true, url: true, if: :activated?
@@ -24,7 +22,7 @@ class ExternalWikiService < Service
end
def execute(_data)
- @response = HTTParty.get(properties['external_wiki_url'], verify: true) rescue nil
+ @response = Gitlab::HTTP.get(properties['external_wiki_url'], verify: true) rescue nil
if @response != 200
nil
end
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/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 5fb15c383ca..df6dcd90985 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -77,13 +77,13 @@ class IssueTrackerService < Service
result = false
begin
- response = HTTParty.head(self.project_url, verify: true)
+ response = Gitlab::HTTP.head(self.project_url, verify: true)
if response
message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}"
result = true
end
- rescue HTTParty::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error
+ rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error
message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}"
end
Rails.logger.info(message)
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 601a6a077f5..ed4bbfb6cfc 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -14,9 +14,8 @@ class JiraService < IssueTrackerService
alias_method :project_url, :url
- # This is confusing, but JiraService does not really support these events.
- # The values here are required to display correct options in the service
- # configuration screen.
+ # When these are false GitLab does not create cross reference
+ # comments on JIRA except when an issue gets transitioned.
def self.supported_events
%w(commit merge_request)
end
@@ -318,4 +317,13 @@ class JiraService < IssueTrackerService
url_changed?
end
+
+ def self.event_description(event)
+ case event
+ when "merge_request", "merge_request_events"
+ "JIRA comments will be created when an issue gets referenced in a merge request."
+ when "commit", "commit_events"
+ "JIRA comments will be created when an issue gets referenced in a commit."
+ end
+ end
end
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index 72ddf9a4be3..2221459c90b 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -52,7 +52,7 @@ class MockCiService < CiService
#
#
def commit_status(sha, ref)
- response = HTTParty.get(commit_status_path(sha), verify: false)
+ response = Gitlab::HTTP.get(commit_status_path(sha), verify: false)
read_commit_status(response)
rescue Errno::ECONNREFUSED
:error
diff --git a/app/models/project_services/packagist_service.rb b/app/models/project_services/packagist_service.rb
index f68a0c1a3c3..ba62a5b7ac0 100644
--- a/app/models/project_services/packagist_service.rb
+++ b/app/models/project_services/packagist_service.rb
@@ -1,6 +1,4 @@
class PackagistService < Service
- include HTTParty
-
prop_accessor :username, :token, :server
validates :username, presence: true, if: :activated?
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index f9dfa2e91c3..3476e7d2283 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -1,6 +1,4 @@
class PivotaltrackerService < Service
- include HTTParty
-
API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'.freeze
prop_accessor :token, :restrict_to_branch
@@ -52,7 +50,7 @@ class PivotaltrackerService < Service
'message' => commit[:message]
}
}
- PivotaltrackerService.post(
+ Gitlab::HTTP.post(
API_ENDPOINT,
body: message.to_json,
headers: {
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index e3a1ca2d45f..8777a44b72f 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -1,6 +1,5 @@
class PushoverService < Service
- include HTTParty
- base_uri 'https://api.pushover.net/1'
+ BASE_URI = 'https://api.pushover.net/1'.freeze
prop_accessor :api_key, :user_key, :device, :priority, :sound
validates :api_key, :user_key, :priority, presence: true, if: :activated?
@@ -99,6 +98,6 @@ class PushoverService < Service
pushover_data[:sound] = sound
end
- PushoverService.post('/messages.json', body: pushover_data)
+ Gitlab::HTTP.post('/messages.json', base_uri: BASE_URI, body: pushover_data)
end
end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index cbe137452bd..145313b8e71 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -83,7 +83,7 @@ class TeamcityService < CiService
branch = Gitlab::Git.ref_name(data[:ref])
- HTTParty.post(
+ Gitlab::HTTP.post(
build_url('httpAuth/app/rest/buildQueue'),
body: "<build branchName=\"#{branch}\">"\
"<buildType id=\"#{build_type}\"/>"\
@@ -134,10 +134,10 @@ class TeamcityService < CiService
end
def get_path(path)
- HTTParty.get(build_url(path), verify: false,
- basic_auth: {
- username: username,
- password: password
- })
+ Gitlab::HTTP.get(build_url(path), verify: false,
+ basic_auth: {
+ username: username,
+ password: password
+ })
end
end
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..2ba1c6cb8c9 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)
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 2556db68146..1dcb79157a2 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -304,6 +304,29 @@ class Service < ActiveRecord::Base
end
end
+ def self.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 "issue", "issue_events"
+ "Event will be triggered when an issue is created/updated/closed"
+ when "confidential_issue", "confidential_issue_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 valid_recipients?
activated? && !importing?
end
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..187878f4fb5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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.
@@ -623,9 +623,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 +1194,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/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/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 3b3d9239086..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)
@@ -65,7 +66,7 @@ module Ci
project.pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.id)
- .where.not(sha: project.repository.sha_from_ref(pipeline.ref))
+ .where.not(sha: project.commit(pipeline.ref).try(:id))
.created_or_pending
end
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/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/notification_service.rb b/app/services/notification_service.rb
index ab94db2c1e5..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
@@ -208,9 +218,9 @@ class NotificationService
def new_access_request(member)
return true unless member.notifiable?(:subscription)
- recipients = member.source.members.active_without_invites.owners_and_masters
+ recipients = member.source.members.active_without_invites_and_requests.owners_and_masters
if fallback_to_group_owners_masters?(recipients, member)
- recipients = member.source.group.members.active_without_invites.owners_and_masters
+ recipients = member.source.group.members.active_without_invites_and_requests.owners_and_masters
end
recipients.each { |recipient| deliver_access_request_email(recipient, member) }
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 81972df9b3c..4b8f955ae69 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -88,7 +88,11 @@ module Projects
def attempt_rollback(project, message)
return unless project
- project.update_attributes(delete_error: message, pending_delete: false)
+ # It's possible that the project was destroyed, but some after_commit
+ # hook failed and caused us to end up here. A destroyed model will be a frozen hash,
+ # which cannot be altered.
+ project.update_attributes(delete_error: message, pending_delete: false) unless project.destroyed?
+
log_error("Deletion failed on #{project.full_path} with the following message: #{message}")
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index f2d676af5c3..a34024f4f80 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -28,7 +28,7 @@ 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)
+ raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
end
# We should skip the repository for a GitHub import or GitLab project import,
diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb
index 52ff64cc938..25017c5cbe3 100644
--- a/app/services/projects/update_pages_configuration_service.rb
+++ b/app/services/projects/update_pages_configuration_service.rb
@@ -18,7 +18,8 @@ module Projects
def pages_config
{
- domains: pages_domains_config
+ domains: pages_domains_config,
+ https_only: project.pages_https_only?
}
end
@@ -27,7 +28,8 @@ module Projects
{
domain: domain.domain,
certificate: domain.certificate,
- key: domain.key
+ key: domain.key,
+ https_only: project.pages_https_only? && domain.https?
}
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 00fdd047208..5bf8208e035 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -81,11 +81,13 @@ module Projects
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)
@@ -103,8 +105,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
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 5f2615a2c01..679f4a9cb62 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -24,6 +24,8 @@ module Projects
system_hook_service.execute_hooks_for(project, :update)
end
+ update_pages_config if changing_pages_https_only?
+
success
else
model_errors = project.errors.full_messages.to_sentence
@@ -67,5 +69,13 @@ module Projects
log_error("Could not create wiki for #{project.full_name}")
Gitlab::Metrics.counter(:wiki_can_not_be_created_total, 'Counts the times we failed to create a wiki')
end
+
+ def update_pages_config
+ Projects::UpdatePagesConfigurationService.new(project).execute
+ end
+
+ def changing_pages_https_only?
+ project.previous_changes.include?(:pages_https_only)
+ end
end
end
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/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb
index 2623f253d98..ac029fad7ea 100644
--- a/app/services/submit_usage_ping_service.rb
+++ b/app/services/submit_usage_ping_service.rb
@@ -14,16 +14,17 @@ class SubmitUsagePingService
def execute
return false unless Gitlab::CurrentSettings.usage_ping_enabled?
- response = HTTParty.post(
+ response = Gitlab::HTTP.post(
URL,
body: Gitlab::UsageData.to_json(force_refresh: true),
+ allow_local_requests: true,
headers: { 'Content-type' => 'application/json' }
)
store_metrics(response)
true
- rescue HTTParty::Error => e
+ rescue Gitlab::HTTP::Error => e
Rails.logger.info "Unable to contact GitLab, Inc.: #{e}"
false
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/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 36e589d5aa8..809ce1303d8 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -3,23 +3,20 @@ class WebHookService
attr_reader :body, :headers, :code
def initialize
- @headers = HTTParty::Response::Headers.new({})
+ @headers = Gitlab::HTTP::Response::Headers.new({})
@body = ''
@code = 'internal error'
end
end
- include HTTParty
-
- # HTTParty timeout
- default_timeout Gitlab.config.gitlab.webhook_timeout
-
- attr_accessor :hook, :data, :hook_name
+ attr_accessor :hook, :data, :hook_name, :request_options
def initialize(hook, data, hook_name)
@hook = hook
@data = data
@hook_name = hook_name.to_s
+ @request_options = { timeout: Gitlab.config.gitlab.webhook_timeout }
+ @request_options.merge!(allow_local_requests: true) if @hook.is_a?(SystemHook)
end
def execute
@@ -73,11 +70,12 @@ class WebHookService
end
def make_request(url, basic_auth = false)
- self.class.post(url,
+ Gitlab::HTTP.post(url,
body: data.to_json,
headers: build_headers(hook_name),
verify: hook.enable_ssl_verification,
- basic_auth: basic_auth)
+ basic_auth: basic_auth,
+ **request_options)
end
def make_request_with_auth
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..30cc4425ae4
--- /dev/null
+++ b/app/uploaders/object_storage.rb
@@ -0,0 +1,421 @@
+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
+ 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) || Store::LOCAL
+ end
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def object_store=(value)
+ @object_store = value || Store::LOCAL
+ @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
+ if file_storage?
+ return yield path
+ end
+
+ begin
+ cache_stored_file!
+ yield cache_path
+ ensure
+ cache_storage.delete_dir!(cache_path(nil))
+ end
+ end
+
+ #
+ # Move the file to another store
+ #
+ # new_store: Enum (Store::LOCAL, Store::REMOTE)
+ #
+ def migrate!(new_store)
+ uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
+ raise 'Already running' unless uuid
+
+ unsafe_migrate!(new_store)
+ ensure
+ Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
+ 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
+
+ #
+ # 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
+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_validator.rb b/app/validators/certificate_validator.rb
index 5239e70a326..b0c9a1b92a4 100644
--- a/app/validators/certificate_validator.rb
+++ b/app/validators/certificate_validator.rb
@@ -16,8 +16,6 @@ class CertificateValidator < ActiveModel::EachValidator
private
def valid_certificate_pem?(value)
- return false unless value
-
OpenSSL::X509::Certificate.new(value).present?
rescue OpenSSL::X509::CertificateError
false
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
index 37a314adee6..3ec1594e202 100644
--- a/app/validators/importable_url_validator.rb
+++ b/app/validators/importable_url_validator.rb
@@ -4,7 +4,7 @@
# protect against Server-side Request Forgery (SSRF).
class ImportableUrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- if Gitlab::UrlBlocker.blocked_url?(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
end
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/_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/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 81d7db04a3c..636535fba84 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -1,298 +1,6 @@
= 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
@@ -302,96 +10,6 @@
= 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.
@@ -860,5 +478,14 @@
.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/_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/_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/_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/_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/_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..17f2f37d24e 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -1,5 +1,106 @@
+- 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
+ = 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
+ = 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
+ = 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
+ = 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
+ = 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
+ = 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
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Auto DevOps, runners amd 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
+ = 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
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Enable and configure Prometheus metrics.')
+ .settings-content
+ = render 'prometheus'
+
+.prepend-top-20
+ = render 'form'
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..5d4229c80af 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?}" } }
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
new file mode 100644
index 00000000000..e0e8fe548d0
--- /dev/null
+++ b/app/views/ide/index.html.haml
@@ -0,0 +1,12 @@
+- @body_class = 'ide'
+- page_title 'IDE'
+
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'ide', force_same_domain: true
+
+#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
+ "no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
+ "committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } }
+ .text-center
+ = icon('spinner spin 2x')
+ %h2.clgray= _('Loading the GitLab IDE...')
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index df5841d1911..dec85368d10 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -13,13 +13,13 @@
.form-group
.input-group
- if current_user.can_select_namespace?
- .input-group-addon
+ .input-group-addon.has-tooltip{ title: root_url }
= root_url
= select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace', tabindex: 1
- else
- .input-group-addon.static-namespace
- #{root_url}#{current_user.username}/
+ .input-group-addon.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
+ #{user_url(current_user.username)}/
= hidden_field_tag :namespace_id, value: current_user.namespace_id
.form-group.col-xs-12.col-sm-6.project-path
= label_tag :path, 'Project name', class: 'label-light'
diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml
index b50537438a9..ddc1cdb24b5 100644
--- a/app/views/layouts/_mailer.html.haml
+++ b/app/views/layouts/_mailer.html.haml
@@ -67,12 +67,8 @@
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
%div
- %a{ href: profile_notifications_url, style: "color:#3777b0;text-decoration:none;" } Manage all notifications
- &middot;
- %a{ href: help_url, style: "color:#3777b0;text-decoration:none;" } Help
- %div
- You're receiving this email because of your account on
- = succeed "." do
- %a{ href: root_url, style: "color:#3777b0;text-decoration:none;" }= Gitlab.config.gitlab.host
+ - manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
+ - help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
+ = _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
= yield :additional_footer
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index f0963cf9da8..f67a8878c80 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -6,6 +6,7 @@
.mobile-overlay
.alert-wrapper
= render "layouts/broadcast"
+ = render 'layouts/header/read_only_banner'
= yield :flash_message
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"
diff --git a/app/views/layouts/header/_read_only_banner.html.haml b/app/views/layouts/header/_read_only_banner.html.haml
new file mode 100644
index 00000000000..f3d563c362f
--- /dev/null
+++ b/app/views/layouts/header/_read_only_banner.html.haml
@@ -0,0 +1,7 @@
+- message = read_only_message
+- if message
+ .flash-container.flash-container-page
+ .flash-notice
+ %div{ class: (container_class) }
+ %span
+ = message
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..5cc6f21c0f3
--- /dev/null
+++ b/app/views/notify/push_to_merge_request_email.html.haml
@@ -0,0 +1,26 @@
+%h3
+ New commits were pushed to the merge request
+ = link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request))
+ by #{@current_user.name}
+
+- 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..d7722e5f41f
--- /dev/null
+++ b/app/views/notify/push_to_merge_request_email.text.haml
@@ -0,0 +1,13 @@
+New commits were pushed to the merge request #{@merge_request.to_reference} by #{@current_user.name}
+\
+#{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/peek/_bar.html.haml b/app/views/peek/_bar.html.haml
new file mode 100644
index 00000000000..b4d86e1601c
--- /dev/null
+++ b/app/views/peek/_bar.html.haml
@@ -0,0 +1,12 @@
+- return unless peek_enabled?
+
+#js-peek{ data: { env: Peek.env,
+ request_id: Peek.request_id,
+ peek_url: peek_routes.results_url,
+ profile_url: url_for(params.merge(lineprofiler: 'true')) },
+ class: Peek.env }
+
+#peek-view-performance-bar.hidden
+ = render_server_response_time
+ %span#serverstats
+ %ul.performance-bar
diff --git a/app/views/peek/views/_gitaly.html.haml b/app/views/peek/views/_gitaly.html.haml
deleted file mode 100644
index 945bb287429..00000000000
--- a/app/views/peek/views/_gitaly.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- local_assigns.fetch(:view)
-
-%button.btn-blank.btn-link.bold{ type: 'button', data: { toggle: 'modal', target: '#modal-peek-gitaly-details' } }
- %span{ data: { defer_to: "#{view.defer_key}-duration" } }...
- \/
- %span{ data: { defer_to: "#{view.defer_key}-calls" } }...
-#modal-peek-gitaly-details.modal{ tabindex: -1, role: 'dialog' }
- .modal-dialog.modal-full
- .modal-content
- .modal-header
- %button.close{ type: 'button', data: { dismiss: 'modal' }, 'aria-label' => 'Close' }
- %span{ 'aria-hidden' => 'true' }
- &times;
- %h4
- Gitaly requests
- .modal-body{ data: { defer_to: "#{view.defer_key}-details" } }...
-gitaly
diff --git a/app/views/peek/views/_host.html.haml b/app/views/peek/views/_host.html.haml
deleted file mode 100644
index 40769b5c6f6..00000000000
--- a/app/views/peek/views/_host.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-%span.current-host
- = truncate(view.hostname)
diff --git a/app/views/peek/views/_mysql2.html.haml b/app/views/peek/views/_mysql2.html.haml
deleted file mode 100644
index ac811a10ef5..00000000000
--- a/app/views/peek/views/_mysql2.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- local_assigns.fetch(:view)
-
-= render 'peek/views/sql', view: view
-mysql
diff --git a/app/views/peek/views/_pg.html.haml b/app/views/peek/views/_pg.html.haml
deleted file mode 100644
index ee94c2f3274..00000000000
--- a/app/views/peek/views/_pg.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- local_assigns.fetch(:view)
-
-= render 'peek/views/sql', view: view
-pg
diff --git a/app/views/peek/views/_rblineprof.html.haml b/app/views/peek/views/_rblineprof.html.haml
deleted file mode 100644
index 6c037930ca9..00000000000
--- a/app/views/peek/views/_rblineprof.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-Profile:
-
-= link_to 'all', url_for(lineprofiler: 'true'), class: 'js-toggle-modal-peek-line-profile'
-\/
-= link_to 'app & lib', url_for(lineprofiler: 'app'), class: 'js-toggle-modal-peek-line-profile'
-\/
-= link_to 'views', url_for(lineprofiler: 'views'), class: 'js-toggle-modal-peek-line-profile'
diff --git a/app/views/peek/views/_sql.html.haml b/app/views/peek/views/_sql.html.haml
deleted file mode 100644
index 36583df898a..00000000000
--- a/app/views/peek/views/_sql.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%button.btn-blank.btn-link.bold{ type: 'button', data: { toggle: 'modal', target: '#modal-peek-pg-queries' } }
- %span{ data: { defer_to: "#{view.defer_key}-duration" } }...
- \/
- %span{ data: { defer_to: "#{view.defer_key}-calls" } }...
-#modal-peek-pg-queries.modal{ tabindex: -1 }
- .modal-dialog.modal-full
- .modal-content
- .modal-header
- %button.close{ type: 'button', data: { dismiss: 'modal' }, 'aria-label' => 'Close' }
- %span{ 'aria-hidden' => 'true' }
- &times;
- %h4
- SQL queries
- .modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }...
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 6f5eb828902..6a1035d2dc7 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -13,6 +13,6 @@
#{time_ago_with_tooltip(event.created_at)}
- .pull-right
+ .flex-right
= link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm qa-create-merge-request" do
#{ _('Create merge request') }
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index f4b5ef1555e..241bc3dbca0 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -9,12 +9,12 @@
Project path
.input-group
- if current_user.can_select_namespace?
- .input-group-addon
+ .input-group-addon.has-tooltip{ title: root_url }
= root_url
= f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1}
- else
- .input-group-addon.static-namespace
+ .input-group-addon.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
#{user_url(current_user.username)}/
= f.hidden_field :namespace_id, value: current_user.namespace_id
.form-group.project-path.col-sm-6
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index f93bb02acb9..1b150ec3e5c 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -12,6 +12,7 @@
.btn-group{ role: "group" }<
= edit_blob_button
+ = ide_edit_button
- if current_user
= replace_blob_link
= delete_blob_link
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 1da0e865a41..883dfb3e6c8 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -5,81 +5,82 @@
- number_commits_behind = diverging_commit_counts[:behind]
- number_commits_ahead = diverging_commit_counts[:ahead]
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
-%li{ class: "js-branch-#{branch.name}" }
- %div
- = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated ref-name' do
- = sprite_icon('fork', size: 12)
- = branch.name
- &nbsp;
- - if branch.name == @repository.root_ref
- %span.label.label-primary default
- - elsif merged
- %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
- = s_('Branches|merged')
+%li{ class: "branch-item js-branch-#{branch.name}" }
+ .branch-info
+ .branch-title
+ = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name' do
+ = sprite_icon('fork', size: 12)
+ = branch.name
+ &nbsp;
+ - if branch.name == @repository.root_ref
+ %span.label.label-primary default
+ - elsif merged
+ %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
+ = s_('Branches|merged')
- - if protected_branch?(@project, branch)
- %span.label.label-success
- = s_('Branches|protected')
- .controls.hidden-xs<
- - if merge_project && create_mr_button?(@repository.root_ref, branch.name)
- = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
- = _('Merge request')
+ - if protected_branch?(@project, branch)
+ %span.label.label-success
+ = s_('Branches|protected')
- - if branch.name != @repository.root_ref
- = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
- class: "btn btn-default #{'prepend-left-10' unless merge_project}",
- method: :post,
- title: s_('Branches|Compare') do
- = s_('Branches|Compare')
+ .block-truncated
+ - if commit
+ = render 'projects/branches/commit', commit: commit, project: @project
+ - else
+ = s_('Branches|Cant find HEAD commit for this branch')
- = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name]
+ - if branch.name != @repository.root_ref
+ .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
+ default_branch: @repository.root_ref,
+ number_commits_ahead: diverging_count_label(number_commits_ahead) } }
+ .graph-side
+ .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
+ %span.count.count-behind= diverging_count_label(number_commits_behind)
+ .graph-separator
+ .graph-side
+ .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
+ %span.count.count-ahead= diverging_count_label(number_commits_ahead)
- - if can?(current_user, :push_code, @project)
- - if branch.name == @project.repository.root_ref
- %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
- disabled: true,
- title: s_('Branches|The default branch cannot be deleted') }
- = icon("trash-o")
- - elsif protected_branch?(@project, branch)
- - if can?(current_user, :delete_protected_branch, @project)
- %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
- title: s_('Branches|Delete protected branch'),
- data: { toggle: "modal",
- target: "#modal-delete-branch",
- delete_path: project_branch_path(@project, branch.name),
- branch_name: branch.name,
- is_merged: ("true" if merged) } }
- = icon("trash-o")
- - else
- %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
- disabled: true,
- title: s_('Branches|Only a project master or owner can delete a protected branch') }
- = icon("trash-o")
- - else
- = link_to project_branch_path(@project, branch.name),
- class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
- title: s_('Branches|Delete branch'),
- method: :delete,
- data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } },
- remote: true,
- 'aria-label' => s_('Branches|Delete branch') do
- = icon("trash-o")
+ .controls.hidden-xs<
+ - if merge_project && create_mr_button?(@repository.root_ref, branch.name)
+ = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
+ = _('Merge request')
- if branch.name != @repository.root_ref
- .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
- default_branch: @repository.root_ref,
- number_commits_ahead: diverging_count_label(number_commits_ahead) } }
- .graph-side
- .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
- %span.count.count-behind= diverging_count_label(number_commits_behind)
- .graph-separator
- .graph-side
- .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
- %span.count.count-ahead= diverging_count_label(number_commits_ahead)
+ = link_to project_compare_index_path(@project, from: @repository.root_ref, to: branch.name),
+ class: "btn btn-default #{'prepend-left-10' unless merge_project}",
+ method: :post,
+ title: s_('Branches|Compare') do
+ = s_('Branches|Compare')
+ = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name]
- - if commit
- = render 'projects/branches/commit', commit: commit, project: @project
- - else
- %p
- = s_('Branches|Cant find HEAD commit for this branch')
+ - if can?(current_user, :push_code, @project)
+ - if branch.name == @project.repository.root_ref
+ %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
+ disabled: true,
+ title: s_('Branches|The default branch cannot be deleted') }
+ = icon("trash-o")
+ - elsif protected_branch?(@project, branch)
+ - if can?(current_user, :delete_protected_branch, @project)
+ %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
+ title: s_('Branches|Delete protected branch'),
+ data: { toggle: "modal",
+ target: "#modal-delete-branch",
+ delete_path: project_branch_path(@project, branch.name),
+ branch_name: branch.name,
+ is_merged: ("true" if merged) } }
+ = icon("trash-o")
+ - else
+ %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
+ disabled: true,
+ title: s_('Branches|Only a project master or owner can delete a protected branch') }
+ = icon("trash-o")
+ - else
+ = link_to project_branch_path(@project, branch.name),
+ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
+ title: s_('Branches|Delete branch'),
+ method: :delete,
+ data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } },
+ remote: true,
+ 'aria-label' => s_('Branches|Delete branch') do
+ = icon("trash-o")
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/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/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index b082ad0ef0e..6fd6018dea3 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -7,9 +7,9 @@
= icon("caret-down", class: "prepend-left-5")
%span.diff-stats-additions-deletions-expanded#diff-stats
with
- %strong.cgreen #{sum_added_lines} additions
+ %strong.cgreen= pluralize(sum_added_lines, 'addition')
and
- %strong.cred #{sum_removed_lines} deletions
+ %strong.cred= pluralize(sum_removed_lines, 'deletion')
.diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
%strong.cgreen<
+#{sum_added_lines}
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index c151b5acdf7..d6f0b230b58 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -14,6 +14,7 @@
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
"empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'),
"empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'),
+ "empty-no-data-svg-path": image_path('illustrations/monitoring/no_data.svg'),
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint": additional_metrics_project_environment_path(@project, @environment, format: :json),
"deployment-endpoint": project_environment_deployments_path(@project, @environment, format: :json),
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 64c648f201b..0c58dd60e2c 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -7,7 +7,9 @@
.issue-main-info
.issue-title.title
%span.issue-title-text
- = confidential_icon(issue)
+ - if issue.confidential?
+ %span.has-tooltip{ title: _('Confidential') }
+ = confidential_icon(issue)
= link_to issue.title, issue_path(issue)
- if issue.tasks?
%span.task-status.hidden-xs
@@ -24,11 +26,11 @@
- if issue.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
- = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(issue.milestone) } do
+ = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(issue) } do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
- %span.issuable-due-date.hidden-xs{ class: "#{'cred' if issue.overdue?}" }
+ %span.issuable-due-date.hidden-xs.has-tooltip{ class: "#{'cred' if issue.overdue?}", title: _('Due date') }
&nbsp;
= icon('calendar')
= issue.due_date.to_s(:medium)
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/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index f45a000833b..a94267deeb2 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -23,11 +23,11 @@
- if merge_request.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
- = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(merge_request.milestone) } do
+ = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(merge_request) } do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
- %span.project-ref-path
+ %span.project-ref-path.has-tooltip{ title: _('Target branch') }
&nbsp;
= link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do
= sprite_icon('fork', size: 12, css_class: 'fork-sprite')
@@ -51,11 +51,11 @@
= render_pipeline_status(merge_request.head_pipeline)
- if merge_request.open? && merge_request.broken?
%li.issuable-pipeline-broken.hidden-xs
- = link_to merge_request_path(merge_request), class: "has-tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
+ = link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do
= icon('exclamation-triangle')
- if merge_request.assignee
%li
- = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
+ = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: _('Assigned to :name'))
= render 'shared/issuable_meta_data', issuable: merge_request
diff --git a/app/views/projects/pages/_https_only.html.haml b/app/views/projects/pages/_https_only.html.haml
new file mode 100644
index 00000000000..6a3ffce949f
--- /dev/null
+++ b/app/views/projects/pages/_https_only.html.haml
@@ -0,0 +1,10 @@
+= form_for @project, url: namespace_project_pages_path(@project.namespace.becomes(Namespace), @project), html: { class: 'inline', title: pages_https_only_title } do |f|
+ = f.check_box :pages_https_only, class: 'pull-left', disabled: pages_https_only_disabled?
+
+ .prepend-left-20
+ = f.label :pages_https_only, class: pages_https_only_label_class do
+ %strong Force domains with SSL certificates to use HTTPS
+
+ - unless pages_https_only_disabled?
+ .prepend-top-10
+ = f.submit 'Save', class: 'btn btn-success'
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index 04e647c0dc6..f17d9d24db6 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -13,6 +13,9 @@
Combined with the power of GitLab CI and the help of GitLab Runner
you can deploy static pages for your individual projects, your user or your group.
+- if Gitlab.config.pages.external_https
+ = render 'https_only'
+
%hr.clearfix
= render 'access'
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/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 06bce52e709..5ef5e9c09a2 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -76,4 +76,8 @@
= render 'projects/find_file_link'
+ = succeed " " do
+ = link_to ide_edit_path(@project, @id, ""), class: 'btn btn-default' do
+ = _('Web IDE')
+
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 435acbc634c..430d9a9dd76 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -5,21 +5,21 @@
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
- if issuable_mr > 0
- %li.issuable-mr.hidden-xs
+ %li.issuable-mr.hidden-xs.has-tooltip{ title: _('Related merge requests') }
= image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged')
= issuable_mr
- if upvotes > 0
- %li.issuable-upvotes.hidden-xs
+ %li.issuable-upvotes.hidden-xs.has-tooltip{ title: _('Upvotes') }
= icon('thumbs-up')
= upvotes
- if downvotes > 0
- %li.issuable-downvotes.hidden-xs
+ %li.issuable-downvotes.hidden-xs.has-tooltip{ title: _('Downvotes') }
= icon('thumbs-down')
= downvotes
%li.issuable-comments.hidden-xs
- = link_to issuable_url, class: ('no-comments' if note_count.zero?) do
+ = link_to issuable_url, class: ['has-tooltip', ('no-comments' if note_count.zero?)], title: _('Comments') do
= icon('comments')
= note_count
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 355b3ac75ae..a41aaed66a3 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -33,7 +33,7 @@
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
%p.light
- = service_event_description(event)
+ = @service.class.event_description(event)
- @service.global_fields.each do |field|
- type = field[:type]
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index 2e9ad380012..149bf8da4b9 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -4,7 +4,7 @@
%header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" }
%h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
%i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable",
- ":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded && list.position === -1, \"fa-caret-left\": !list.isExpanded && list.position !== -1 }",
+ ":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded }",
"aria-hidden": "true" }
%span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"",
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/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..01ed123e6c8
--- /dev/null
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -0,0 +1,202 @@
+# 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, mounted_as, to_store)
+ sanity_check!(uploads, mounted_as)
+
+ perform_async(uploads.ids, 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, mounted_as)
+ upload = uploads.first
+
+ uploader_class = upload.uploader.constantize
+ model_class = uploads.first.model_type.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(ids, mounted_as, to_store)
+ @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, @mounted_as)
+ 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/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 07584fab7c8..712a63af532 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,3 +1,4 @@
+# Gitaly issue: https://gitlab.com/gitlab-org/gitaly/issues/1110
class RepositoryForkWorker
include ApplicationWorker
include Gitlab::ShellAdapter
diff --git a/bin/rails b/bin/rails
index 0138d79b751..228f812ccaf 100755
--- a/bin/rails
+++ b/bin/rails
@@ -1,9 +1,14 @@
#!/usr/bin/env ruby
-begin
- load File.expand_path('../spring', __FILE__)
-rescue LoadError => e
- raise unless e.message.include?('spring')
+
+# Remove this block when upgraded to rails 5.0.
+unless %w[1 true].include?(ENV["RAILS5"])
+ begin
+ load File.expand_path('../spring', __FILE__)
+ rescue LoadError => e
+ raise unless e.message.include?('spring')
+ end
end
-APP_PATH = File.expand_path('../../config/application', __FILE__)
+
+APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
diff --git a/bin/rake b/bin/rake
index d87d5f57810..b52a8321f1a 100755
--- a/bin/rake
+++ b/bin/rake
@@ -1,9 +1,14 @@
#!/usr/bin/env ruby
-begin
- load File.expand_path('../spring', __FILE__)
-rescue LoadError => e
- raise unless e.message.include?('spring')
+
+# Remove this block when upgraded to rails 5.0.
+unless %w[1 true].include?(ENV["RAILS5"])
+ begin
+ load File.expand_path('../spring', __FILE__)
+ rescue LoadError => e
+ raise unless e.message.include?('spring')
+ end
end
+
require_relative '../config/boot'
require 'rake'
Rake.application.run
diff --git a/bin/setup b/bin/setup
index 6cb2d7f1e3a..c60c1267e06 100755
--- a/bin/setup
+++ b/bin/setup
@@ -1,29 +1,61 @@
#!/usr/bin/env ruby
-require 'pathname'
+
+def rails5?
+ %w[1 true].include?(ENV["RAILS5"])
+end
+
+require "pathname"
# path to your application root.
-APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+APP_ROOT = Pathname.new File.expand_path("../../", __FILE__)
+
+if rails5?
+ def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+ end
+end
Dir.chdir APP_ROOT do
# This script is a starting point to setup your application.
# Add necessary setup steps to this file:
puts "== Installing dependencies =="
- system "gem install bundler --conservative"
- system "bundle check || bundle install"
+
+ if rails5?
+ system! "gem install bundler --conservative"
+ system("bundle check") || system!("bundle install")
+ else
+ system "gem install bundler --conservative"
+ system "bundle check || bundle install"
+ end
# puts "\n== Copying sample files =="
# unless File.exist?("config/database.yml")
- # system "cp config/database.yml.sample config/database.yml"
+ # cp "config/database.yml.sample", "config/database.yml"
# end
puts "\n== Preparing database =="
- system "bin/rake db:reset"
+
+ if rails5?
+ system! "bin/rails db:setup"
+ else
+ system "bin/rake db:reset"
+ end
puts "\n== Removing old logs and tempfiles =="
- system "rm -f log/*"
- system "rm -rf tmp/cache"
+
+ if rails5?
+ system! "bin/rails log:clear tmp:clear"
+ else
+ system "rm -f log/*"
+ system "rm -rf tmp/cache"
+ end
puts "\n== Restarting application server =="
- system "touch tmp/restart.txt"
+
+ if rails5?
+ system! "bin/rails restart"
+ else
+ system "touch tmp/restart.txt"
+ end
end
diff --git a/bin/update b/bin/update
new file mode 100755
index 00000000000..a8e4462f203
--- /dev/null
+++ b/bin/update
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+require 'pathname'
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+chdir APP_ROOT do
+ # This script is a way to update your development environment automatically.
+ # Add necessary update steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ puts "\n== Updating database =="
+ system! 'bin/rails db:migrate'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml b/changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml
deleted file mode 100644
index a38b447e345..00000000000
--- a/changelogs/unreleased-ee/39118-dynamic-pipeline-variables-fe.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Update CI/CD secret variables list to be dynamic and save without reloading
- the page
-merge_request: 4110
-author:
-type: added
diff --git a/changelogs/unreleased-ee/4378-fix-cluster-js-not-running-on-update-page.yml b/changelogs/unreleased-ee/4378-fix-cluster-js-not-running-on-update-page.yml
deleted file mode 100644
index bbb6cbd05be..00000000000
--- a/changelogs/unreleased-ee/4378-fix-cluster-js-not-running-on-update-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix JavaScript bundle running on Cluster update/destroy pages
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased-ee/bvl-external-policy-classification.yml b/changelogs/unreleased-ee/bvl-external-policy-classification.yml
deleted file mode 100644
index 074629c8c12..00000000000
--- a/changelogs/unreleased-ee/bvl-external-policy-classification.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Authorize project access with an external service
-merge_request: 4675
-author:
-type: added
diff --git a/changelogs/unreleased/17203-add-missing-pagination-commit-diff-endpoint.yml b/changelogs/unreleased/17203-add-missing-pagination-commit-diff-endpoint.yml
deleted file mode 100644
index efd936ca104..00000000000
--- a/changelogs/unreleased/17203-add-missing-pagination-commit-diff-endpoint.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
- title: Add missing pagination on the commit diff endpoint
- merge_request: 17203
- author: Maxime Roussin-Bélanger
- type: fixed
diff --git a/changelogs/unreleased/17359-move-oauth-modules-to-auth-dir-structure.yml b/changelogs/unreleased/17359-move-oauth-modules-to-auth-dir-structure.yml
deleted file mode 100644
index ca049f9edaa..00000000000
--- a/changelogs/unreleased/17359-move-oauth-modules-to-auth-dir-structure.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Moved o_auth/saml/ldap modules under gitlab/auth
-merge_request: 17359
-author: Horatiu Eugen Vlad
diff --git a/changelogs/unreleased/17500-mr-multiple-issues-oxford-comma.yml b/changelogs/unreleased/17500-mr-multiple-issues-oxford-comma.yml
deleted file mode 100644
index a94e6153a05..00000000000
--- a/changelogs/unreleased/17500-mr-multiple-issues-oxford-comma.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update issue closing pattern to allow variations in punctuation
-merge_request: 17198
-author: Vicky Chijwani
-type: changed
diff --git a/changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml b/changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml
new file mode 100644
index 00000000000..a62137ea2c9
--- /dev/null
+++ b/changelogs/unreleased/23460-send-email-when-pushing-more-commits-to-the-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: Send notification emails when push to a merge request
+merge_request: 7610
+author: YarNayar
+type: feature
diff --git a/changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml b/changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml
deleted file mode 100644
index b909bb2d021..00000000000
--- a/changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clear the Labels dropdown search filter after a selection is made
-merge_request: 17393
-author: Andrew Torres
-type: changed
diff --git a/changelogs/unreleased/26039-Update-to-github-linguist5-3-x.yml b/changelogs/unreleased/26039-Update-to-github-linguist5-3-x.yml
deleted file mode 100644
index 0f1cb2fef9d..00000000000
--- a/changelogs/unreleased/26039-Update-to-github-linguist5-3-x.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update to github-linguist 5.3.x
-merge_request: 17241
-author: Ken Ding
-type: other
diff --git a/changelogs/unreleased/26466-natural-sort-mrs.yml b/changelogs/unreleased/26466-natural-sort-mrs.yml
deleted file mode 100644
index e3bf9834f24..00000000000
--- a/changelogs/unreleased/26466-natural-sort-mrs.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Group MRs on issue page by project and namespace.
-merge_request: 8494
-author: Jeff Stubler
diff --git a/changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml b/changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml
new file mode 100644
index 00000000000..d96f7e54c8d
--- /dev/null
+++ b/changelogs/unreleased/27210-add-cancel-btn-to-new-page-domain.yml
@@ -0,0 +1,5 @@
+---
+title: Adds cancel btn to new pages domain page
+merge_request: 18026
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/29130-api-project-export.yml b/changelogs/unreleased/29130-api-project-export.yml
deleted file mode 100644
index 7dee349232a..00000000000
--- a/changelogs/unreleased/29130-api-project-export.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add project export API
-merge_request: 15860
-author: Travis Miller
-type: added
diff --git a/changelogs/unreleased/29497-pages-custom-domain-dns-verification.yml b/changelogs/unreleased/29497-pages-custom-domain-dns-verification.yml
deleted file mode 100644
index f958f3f1272..00000000000
--- a/changelogs/unreleased/29497-pages-custom-domain-dns-verification.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add verification for GitLab Pages custom domains
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/30665-add-email-button-to-new-issue-by-email.yml b/changelogs/unreleased/30665-add-email-button-to-new-issue-by-email.yml
deleted file mode 100644
index 175b3103d90..00000000000
--- a/changelogs/unreleased/30665-add-email-button-to-new-issue-by-email.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add email button to new issue by email
-merge_request: 10942
-author: Islam Wazery
diff --git a/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml b/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml
new file mode 100644
index 00000000000..bc1955bc66f
--- /dev/null
+++ b/changelogs/unreleased/31114-internal-ids-are-not-atomic.yml
@@ -0,0 +1,5 @@
+---
+title: Atomic generation of internal ids for issues.
+merge_request: 17580
+author:
+type: other
diff --git a/changelogs/unreleased/32564-fix-double-system-closing-notes.yml b/changelogs/unreleased/32564-fix-double-system-closing-notes.yml
deleted file mode 100644
index e6e1ef8c76d..00000000000
--- a/changelogs/unreleased/32564-fix-double-system-closing-notes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix duplicate system notes when merging a merge request.
-merge_request: 17035
-author:
-type: fixed
diff --git a/changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml b/changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml
deleted file mode 100644
index 74675992105..00000000000
--- a/changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow installation of GitLab Runner with a single click
-merge_request: 17134
-author:
-type: added
diff --git a/changelogs/unreleased/33570-slack-notify-default-branch.yml b/changelogs/unreleased/33570-slack-notify-default-branch.yml
deleted file mode 100644
index 5c90ce47729..00000000000
--- a/changelogs/unreleased/33570-slack-notify-default-branch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Slack/Mattermost notifications not respecting `notify_only_default_branch` setting for pushes
-merge_request: 17345
-author:
-type: fixed
diff --git a/changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml b/changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml
new file mode 100644
index 00000000000..0382ede4565
--- /dev/null
+++ b/changelogs/unreleased/33803-drop-json-support-in-project-milestone.yml
@@ -0,0 +1,5 @@
+---
+title: Drop JSON response in Project Milestone along with avoiding error
+merge_request: 17977
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/35418-remove-underline-for-avatar.yml b/changelogs/unreleased/35418-remove-underline-for-avatar.yml
deleted file mode 100644
index 034365e1137..00000000000
--- a/changelogs/unreleased/35418-remove-underline-for-avatar.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: remove avater underline
-merge_request: 17219
-author: Ken Ding
-type: fixed
diff --git a/changelogs/unreleased/35530-teleporting-emoji.yml b/changelogs/unreleased/35530-teleporting-emoji.yml
deleted file mode 100644
index a60a42b9e48..00000000000
--- a/changelogs/unreleased/35530-teleporting-emoji.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Teleporting Emoji
-merge_request: 16963
-author: Jared Deckard <jared.deckard@gmail.com>
-type: fixed
diff --git a/changelogs/unreleased/36847-update-update-toml-rb-to-1-0-0.yml b/changelogs/unreleased/36847-update-update-toml-rb-to-1-0-0.yml
deleted file mode 100644
index 74eaf57c056..00000000000
--- a/changelogs/unreleased/36847-update-update-toml-rb-to-1-0-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: update toml-rb to 1.0.0
-merge_request: 17259
-author: Ken Ding
-type: other
diff --git a/changelogs/unreleased/37050-ext-issue-tracker.yml b/changelogs/unreleased/37050-ext-issue-tracker.yml
deleted file mode 100644
index 29bccdded02..00000000000
--- a/changelogs/unreleased/37050-ext-issue-tracker.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display a link to external issue tracker when enabled
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml b/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml
new file mode 100644
index 00000000000..cec06bf2dfe
--- /dev/null
+++ b/changelogs/unreleased/38167-ui-bug-when-creating-new-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed bug in dropdown selector when selecting the same selection again
+merge_request: 14631
+author: bitsapien
+type: fixed
diff --git a/changelogs/unreleased/38587-pipelines-empty-state.yml b/changelogs/unreleased/38587-pipelines-empty-state.yml
deleted file mode 100644
index 58ea204d394..00000000000
--- a/changelogs/unreleased/38587-pipelines-empty-state.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Handle empty state in Pipelines page
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/39444-make-margin-around-dropdown-dividers-4px.yml b/changelogs/unreleased/39444-make-margin-around-dropdown-dividers-4px.yml
deleted file mode 100644
index da65cfff799..00000000000
--- a/changelogs/unreleased/39444-make-margin-around-dropdown-dividers-4px.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Set margins around dropdown dividers to 4px
-merge_request: 17517
-author:
-type: fixed
diff --git a/changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml b/changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml
new file mode 100644
index 00000000000..30a8dc63983
--- /dev/null
+++ b/changelogs/unreleased/39584-nesting-depth-5-framework-dropdowns.yml
@@ -0,0 +1,5 @@
+---
+title: Apply NestingDepth (level 5) (framework/dropdowns.scss)
+merge_request: 17820
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/39607-fix-avatar--vertical-align.yml b/changelogs/unreleased/39607-fix-avatar--vertical-align.yml
deleted file mode 100644
index 4d9fee12f04..00000000000
--- a/changelogs/unreleased/39607-fix-avatar--vertical-align.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "Fix user avatar's vertical align on the issues and merge requests pages"
-merge_request: 17072
-author: Laszlo Karpati
-type: fixed
diff --git a/changelogs/unreleased/40187-project-branch-dashboard-with-active-stale-branches.yml b/changelogs/unreleased/40187-project-branch-dashboard-with-active-stale-branches.yml
deleted file mode 100644
index 3833aab42dd..00000000000
--- a/changelogs/unreleased/40187-project-branch-dashboard-with-active-stale-branches.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add overview of branches and a filter for active/stale branches
-merge_request: 15402
-author: Takuya Noguchi
-type: added
diff --git a/changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml b/changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml
deleted file mode 100644
index dddd8473df5..00000000000
--- a/changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Keep link when redacting unauthorized object links
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/40525-listing-user-activity-timeouts.yml b/changelogs/unreleased/40525-listing-user-activity-timeouts.yml
deleted file mode 100644
index 39ce873dba6..00000000000
--- a/changelogs/unreleased/40525-listing-user-activity-timeouts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve database response time for user activity listing.
-merge_request: 17454
-author:
-type: performance
diff --git a/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml b/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml
deleted file mode 100644
index 9e4811ca308..00000000000
--- a/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Sanitize extra blank spaces used when uploading a SSH key
-merge_request: 40552
-author:
-type: fixed
diff --git a/changelogs/unreleased/40623-fix-404-when-listing-archived-projects-in-a-group-where-all-projects-have-been-archived.yml b/changelogs/unreleased/40623-fix-404-when-listing-archived-projects-in-a-group-where-all-projects-have-been-archived.yml
deleted file mode 100644
index 543fd7c5e8d..00000000000
--- a/changelogs/unreleased/40623-fix-404-when-listing-archived-projects-in-a-group-where-all-projects-have-been-archived.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-title: Fix 404 when listing archived projects in a group where all projects have been archived
-merge_request: 17077
-author: Ashley Dumaine
-type: fixed
diff --git a/changelogs/unreleased/40668-pages-domain-api-returns-404-when-using-a-specific-domain.yml b/changelogs/unreleased/40668-pages-domain-api-returns-404-when-using-a-specific-domain.yml
deleted file mode 100644
index d77572d6175..00000000000
--- a/changelogs/unreleased/40668-pages-domain-api-returns-404-when-using-a-specific-domain.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix get a single pages domain when project path contains a period
-merge_request: 17206
-author: Travis Miller
-type: fixed
diff --git a/changelogs/unreleased/40781-os-to-ce.yml b/changelogs/unreleased/40781-os-to-ce.yml
new file mode 100644
index 00000000000..4a364292c60
--- /dev/null
+++ b/changelogs/unreleased/40781-os-to-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Add object storage support for LFS objects, CI artifacts, and uploads.
+merge_request: 17358
+author:
+type: added
diff --git a/changelogs/unreleased/40994-expose-features-as-ci-cd-variable.yml b/changelogs/unreleased/40994-expose-features-as-ci-cd-variable.yml
deleted file mode 100644
index 1e377094791..00000000000
--- a/changelogs/unreleased/40994-expose-features-as-ci-cd-variable.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Expose GITLAB_FEATURES as CI/CD variable (fixes #40994)'
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/41616-api-issues-between-date.yml b/changelogs/unreleased/41616-api-issues-between-date.yml
deleted file mode 100644
index d8a23f48699..00000000000
--- a/changelogs/unreleased/41616-api-issues-between-date.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds updated_at filter to issues and merge_requests API
-merge_request: 17417
-author: Jacopo Beschi @jacopo-beschi
-type: added
diff --git a/changelogs/unreleased/41719-mr-title-fix.yml b/changelogs/unreleased/41719-mr-title-fix.yml
deleted file mode 100644
index 92388f30cb2..00000000000
--- a/changelogs/unreleased/41719-mr-title-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Render htmlentities correctly for links not supported by Rinku
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/41777-include-cycle-time-in-usage-ping.yml b/changelogs/unreleased/41777-include-cycle-time-in-usage-ping.yml
deleted file mode 100644
index 8d8a5dfefa3..00000000000
--- a/changelogs/unreleased/41777-include-cycle-time-in-usage-ping.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include cycle time in usage ping data
-merge_request: 16973
-author:
-type: added
diff --git a/changelogs/unreleased/41851-enable-eslint-codeclimate.yml b/changelogs/unreleased/41851-enable-eslint-codeclimate.yml
deleted file mode 100644
index 98924f3eae8..00000000000
--- a/changelogs/unreleased/41851-enable-eslint-codeclimate.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enables eslint in codeclimate job
-merge_request: 17392
-author:
-type: other
diff --git a/changelogs/unreleased/41899-api-endpoint-for-importing-a-project-export.yml b/changelogs/unreleased/41899-api-endpoint-for-importing-a-project-export.yml
deleted file mode 100644
index 29ab7cc7cab..00000000000
--- a/changelogs/unreleased/41899-api-endpoint-for-importing-a-project-export.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: API endpoint for importing a project export
-merge_request: 17025
-author:
-type: added
diff --git a/changelogs/unreleased/41905_merge_request_and_issue_metrics.yml b/changelogs/unreleased/41905_merge_request_and_issue_metrics.yml
deleted file mode 100644
index c9e23360e3b..00000000000
--- a/changelogs/unreleased/41905_merge_request_and_issue_metrics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: expose more metrics in merge requests api
-merge_request: 16589
-author: haseebeqx
-type: added
diff --git a/changelogs/unreleased/41949-move.yml b/changelogs/unreleased/41949-move.yml
deleted file mode 100644
index 40ccac63a28..00000000000
--- a/changelogs/unreleased/41949-move.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remember assignee when moving an issue
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml b/changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml
new file mode 100644
index 00000000000..f7758734a6f
--- /dev/null
+++ b/changelogs/unreleased/42037-long-instance-names-group-names-covers-namespace-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Long instance urls do not overflow anymore during project creation
+merge_request: 17717
+author:
+type: fixed
diff --git a/changelogs/unreleased/42044-osw-add-button-to-deploy-runner-to-kubernetes.yml b/changelogs/unreleased/42044-osw-add-button-to-deploy-runner-to-kubernetes.yml
deleted file mode 100644
index 6cf0de5b3fa..00000000000
--- a/changelogs/unreleased/42044-osw-add-button-to-deploy-runner-to-kubernetes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add a button to deploy a runner to a Kubernetes cluster in the settings page
-merge_request: 17278
-author:
-type: changed
diff --git a/changelogs/unreleased/42274-group-request-membership-long-too.yml b/changelogs/unreleased/42274-group-request-membership-long-too.yml
deleted file mode 100644
index 03efedba638..00000000000
--- a/changelogs/unreleased/42274-group-request-membership-long-too.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix long list of recipients on group request membership email
-merge_request: 17121
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/42314-diff-file.yml b/changelogs/unreleased/42314-diff-file.yml
deleted file mode 100644
index 1eed5ef1a34..00000000000
--- a/changelogs/unreleased/42314-diff-file.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Render modified icon for moved file in changes dropdown
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/42332-actionview-template-error-366-524-out-of-range.yml b/changelogs/unreleased/42332-actionview-template-error-366-524-out-of-range.yml
deleted file mode 100644
index 626c761bfbd..00000000000
--- a/changelogs/unreleased/42332-actionview-template-error-366-524-out-of-range.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix 500 error being shown when diff has context marker with invalid encoding
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/42431-add-auto-devops-and-clusters-button-to-projects.yml b/changelogs/unreleased/42431-add-auto-devops-and-clusters-button-to-projects.yml
deleted file mode 100644
index 5613b2af763..00000000000
--- a/changelogs/unreleased/42431-add-auto-devops-and-clusters-button-to-projects.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Add a button on the project page to set up a Kubernetes cluster and enable
- Auto DevOps
-merge_request: 16900
-author:
-type: added
diff --git a/changelogs/unreleased/42434-allow-commits-endpoint-to-work-over-all-commits.yml b/changelogs/unreleased/42434-allow-commits-endpoint-to-work-over-all-commits.yml
deleted file mode 100644
index c596a88ba0b..00000000000
--- a/changelogs/unreleased/42434-allow-commits-endpoint-to-work-over-all-commits.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow commits endpoint to work over all commits of a repository
-merge_request: 17182
-author:
-type: added
diff --git a/changelogs/unreleased/42481-remove-notification-settings-left-projects.yml b/changelogs/unreleased/42481-remove-notification-settings-left-projects.yml
deleted file mode 100644
index ea99649131b..00000000000
--- a/changelogs/unreleased/42481-remove-notification-settings-left-projects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove user notification settings for groups and projects when user leaves
-merge_request: 16906
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/42509-fix-API-PUT-projects-fails-when-only-ci_config_path-is-specified.yml b/changelogs/unreleased/42509-fix-API-PUT-projects-fails-when-only-ci_config_path-is-specified.yml
deleted file mode 100644
index a3dc1917001..00000000000
--- a/changelogs/unreleased/42509-fix-API-PUT-projects-fails-when-only-ci_config_path-is-specified.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow to call PUT /projects/:id API with only ci_config_path specified
-merge_request: 17105
-author: Laszlo Karpati
-type: fixed
diff --git a/changelogs/unreleased/42545-milestion-quick-actions-for-groups.yml b/changelogs/unreleased/42545-milestion-quick-actions-for-groups.yml
deleted file mode 100644
index d29f79aaaf8..00000000000
--- a/changelogs/unreleased/42545-milestion-quick-actions-for-groups.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allows the usage of /milestone quick action for group milestones
-merge_request: 17239
-author: Jacopo Beschi @jacopo-beschi
-type: fixed
diff --git a/changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml b/changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml
deleted file mode 100644
index 35457db82f4..00000000000
--- a/changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display ingress IP address in the Kubernetes page
-merge_request: 17052
-author:
-type: added
diff --git a/changelogs/unreleased/42712_api_branches_add_search_param_20180207.yml b/changelogs/unreleased/42712_api_branches_add_search_param_20180207.yml
deleted file mode 100644
index 609b5ce48ef..00000000000
--- a/changelogs/unreleased/42712_api_branches_add_search_param_20180207.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add search param to Branches API
-merge_request: 17005
-author: bunufi
-type: added
diff --git a/changelogs/unreleased/42800-change-usage-of-avatar_icon.yml b/changelogs/unreleased/42800-change-usage-of-avatar_icon.yml
deleted file mode 100644
index 00f4b7436a7..00000000000
--- a/changelogs/unreleased/42800-change-usage-of-avatar_icon.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Use a user object in ApplicationHelper#avatar_icon where possible to avoid
- N+1 queries.
-merge_request: 42800
-author:
-type: performance
diff --git a/changelogs/unreleased/42814-fix-remove-source-branch-when-mwps.yml b/changelogs/unreleased/42814-fix-remove-source-branch-when-mwps.yml
deleted file mode 100644
index 08e77ee7c3b..00000000000
--- a/changelogs/unreleased/42814-fix-remove-source-branch-when-mwps.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix "Remove source branch" button in Merge request widget during merge when pipeline
- succeeds state
-merge_request: 17192
-author:
-type: fixed
diff --git a/changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml b/changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml
new file mode 100644
index 00000000000..0e892a51bc5
--- /dev/null
+++ b/changelogs/unreleased/42880-loss-of-input-text-on-comments-after-preview.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Firefox stealing formatting characters on issue notes
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/42921-ci-charts-include-current-day.yml b/changelogs/unreleased/42921-ci-charts-include-current-day.yml
deleted file mode 100644
index d0de6665735..00000000000
--- a/changelogs/unreleased/42921-ci-charts-include-current-day.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: CI charts now include the current day
-merge_request: 17032
-author: Dakkaron
-type: changed
diff --git a/changelogs/unreleased/42922-environment-name.yml b/changelogs/unreleased/42922-environment-name.yml
deleted file mode 100644
index 0e9544245f6..00000000000
--- a/changelogs/unreleased/42922-environment-name.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Adds tooltip in environment names to increase readability
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/42923-close-issue.yml b/changelogs/unreleased/42923-close-issue.yml
deleted file mode 100644
index e332bbf5dec..00000000000
--- a/changelogs/unreleased/42923-close-issue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix close button on issues not working on mobile
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/42929-hide-new-variable-values.yml b/changelogs/unreleased/42929-hide-new-variable-values.yml
deleted file mode 100644
index 68decd25b5a..00000000000
--- a/changelogs/unreleased/42929-hide-new-variable-values.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hide CI secret variable values after saving
-merge_request: 17044
-author:
-type: changed
diff --git a/changelogs/unreleased/42946-update-pipeline-cancel-tooltip-to-stop.yml b/changelogs/unreleased/42946-update-pipeline-cancel-tooltip-to-stop.yml
deleted file mode 100644
index 0e566dd0abf..00000000000
--- a/changelogs/unreleased/42946-update-pipeline-cancel-tooltip-to-stop.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update tooltip on pipeline cancel to Stop (#42946)
-merge_request: 17444
-author:
-type: fixed
diff --git a/changelogs/unreleased/43134-reduce-queries-pipelines-controller-show.yml b/changelogs/unreleased/43134-reduce-queries-pipelines-controller-show.yml
deleted file mode 100644
index c1e9614b676..00000000000
--- a/changelogs/unreleased/43134-reduce-queries-pipelines-controller-show.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance of pipeline page by reducing DB queries
-merge_request: 17168
-author:
-type: performance
diff --git a/changelogs/unreleased/43198-fix-settings-panel-expanding-when-fragment-hash-linked.yml b/changelogs/unreleased/43198-fix-settings-panel-expanding-when-fragment-hash-linked.yml
deleted file mode 100644
index 49ba48a0fef..00000000000
--- a/changelogs/unreleased/43198-fix-settings-panel-expanding-when-fragment-hash-linked.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix settings panels not expanding when fragment hash linked
-merge_request: 17074
-author:
-type: fixed
diff --git a/changelogs/unreleased/43201-rename-repository-submit-button-disabled.yml b/changelogs/unreleased/43201-rename-repository-submit-button-disabled.yml
deleted file mode 100644
index b527000332e..00000000000
--- a/changelogs/unreleased/43201-rename-repository-submit-button-disabled.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allows project rename after validation error
-merge_request: 17150
-author:
-type: fixed
diff --git a/changelogs/unreleased/43261-fix-import-from-url-name-collision-active-tab.yml b/changelogs/unreleased/43261-fix-import-from-url-name-collision-active-tab.yml
deleted file mode 100644
index 71073b2e214..00000000000
--- a/changelogs/unreleased/43261-fix-import-from-url-name-collision-active-tab.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Keep "Import project" tab/form active when validation fails trying to import
- "Repo by URL"
-merge_request: 17136
-author:
-type: fixed
diff --git a/changelogs/unreleased/43275-improve-variables-validation-message.yml b/changelogs/unreleased/43275-improve-variables-validation-message.yml
deleted file mode 100644
index 88ef93123a0..00000000000
--- a/changelogs/unreleased/43275-improve-variables-validation-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove duplicated error message on duplicate variable validation
-merge_request: 17135
-author:
-type: fixed
diff --git a/changelogs/unreleased/43315-gpg-popover.yml b/changelogs/unreleased/43315-gpg-popover.yml
deleted file mode 100644
index 69238aa8075..00000000000
--- a/changelogs/unreleased/43315-gpg-popover.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes gpg popover layout
-merge_request: 17323
-author:
-type: fixed
diff --git a/changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml b/changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml
new file mode 100644
index 00000000000..de1cee6e436
--- /dev/null
+++ b/changelogs/unreleased/43316-controller-parameters-handling-sensitive-information-should-use-a-more-specific-name.yml
@@ -0,0 +1,5 @@
+---
+title: Use specific names for filtered CI variable controller parameters
+merge_request: 17796
+author:
+type: other
diff --git a/changelogs/unreleased/43334-reply-by-email-did-not-pick-up-unsubscribe-quick-action.yml b/changelogs/unreleased/43334-reply-by-email-did-not-pick-up-unsubscribe-quick-action.yml
deleted file mode 100644
index 86be5ee1804..00000000000
--- a/changelogs/unreleased/43334-reply-by-email-did-not-pick-up-unsubscribe-quick-action.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix quick actions for users who cannot update issues and merge requests
-merge_request: 17482
-author:
-type: fixed
diff --git a/changelogs/unreleased/43460-track-projects-a-user-interacted-with.yml b/changelogs/unreleased/43460-track-projects-a-user-interacted-with.yml
deleted file mode 100644
index 99b6ac76a3e..00000000000
--- a/changelogs/unreleased/43460-track-projects-a-user-interacted-with.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Keep track of projects a user interacted with.
-merge_request: 17327
-author:
-type: other
diff --git a/changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml b/changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml
new file mode 100644
index 00000000000..889fd008bad
--- /dev/null
+++ b/changelogs/unreleased/43482-enabling-auto-devops-on-an-empty-project-gives-you-wrong-information.yml
@@ -0,0 +1,5 @@
+---
+title: Add empty repo check before running AutoDevOps pipeline
+merge_request: 17605
+author:
+type: changed
diff --git a/changelogs/unreleased/43489-display-runner-ip.yml b/changelogs/unreleased/43489-display-runner-ip.yml
deleted file mode 100644
index 621c2ec709a..00000000000
--- a/changelogs/unreleased/43489-display-runner-ip.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display Runner IP Address
-merge_request: 17286
-author:
-type: added
diff --git a/changelogs/unreleased/43496-error-message-for-gke-clusters-persists-in-the-next-page.yml b/changelogs/unreleased/43496-error-message-for-gke-clusters-persists-in-the-next-page.yml
deleted file mode 100644
index c10b0e7a3cf..00000000000
--- a/changelogs/unreleased/43496-error-message-for-gke-clusters-persists-in-the-next-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Do not persist Google Project verification flash errors after a page reload
-merge_request: 17299
-author:
-type: fixed
diff --git a/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml b/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml
new file mode 100644
index 00000000000..039d3de7168
--- /dev/null
+++ b/changelogs/unreleased/43512-add-support-for-omniauth-jwt-provider.yml
@@ -0,0 +1,5 @@
+---
+title: Adds support for OmniAuth JWT provider
+merge_request: 17774
+author:
+type: added
diff --git a/changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml b/changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml
new file mode 100644
index 00000000000..f30fea3c4a7
--- /dev/null
+++ b/changelogs/unreleased/43525-limit-number-of-failed-logins-using-ldap.yml
@@ -0,0 +1,5 @@
+---
+title: Limit the number of failed logins when using LDAP for authentication
+merge_request: 43525
+author:
+type: added
diff --git a/changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml b/changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml
new file mode 100644
index 00000000000..39f92c281ad
--- /dev/null
+++ b/changelogs/unreleased/43552-user-owned-projects-query-performance-improvement.yml
@@ -0,0 +1,5 @@
+---
+title: Improves the performance of projects list page
+merge_request: 17934
+author:
+type: performance
diff --git a/changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml b/changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml
deleted file mode 100644
index bda4ec84e5c..00000000000
--- a/changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Group labels load failure when there are duplicate labels present
-merge_request: 17353
-author:
-type: fixed
diff --git a/changelogs/unreleased/43603-ci-lint-support.yml b/changelogs/unreleased/43603-ci-lint-support.yml
new file mode 100644
index 00000000000..8e4a92c0287
--- /dev/null
+++ b/changelogs/unreleased/43603-ci-lint-support.yml
@@ -0,0 +1,5 @@
+---
+title: Move ci/lint under project's namespace
+merge_request: 17729
+author:
+type: added
diff --git a/changelogs/unreleased/43643-fix-mr-label-filtering.yml b/changelogs/unreleased/43643-fix-mr-label-filtering.yml
deleted file mode 100644
index 32a44aef243..00000000000
--- a/changelogs/unreleased/43643-fix-mr-label-filtering.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable filtering MR list based on clicked label in MR sidebar
-merge_request: 17390
-author:
-type: fixed
diff --git a/changelogs/unreleased/43771-improve-avatar-error-message.yml b/changelogs/unreleased/43771-improve-avatar-error-message.yml
new file mode 100644
index 00000000000..1fae10f4d1f
--- /dev/null
+++ b/changelogs/unreleased/43771-improve-avatar-error-message.yml
@@ -0,0 +1,5 @@
+---
+title: Change avatar error message to include allowed file formats
+merge_request: 17747
+author: Fabian Schneider
+type: changed
diff --git a/changelogs/unreleased/43780-add-a-paragraph-about-clusters-security-implications.yml b/changelogs/unreleased/43780-add-a-paragraph-about-clusters-security-implications.yml
deleted file mode 100644
index 0fa21a2013c..00000000000
--- a/changelogs/unreleased/43780-add-a-paragraph-about-clusters-security-implications.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add a paragraph about security implications on Cluster's page
-merge_request: 17486
-author:
-type: added
diff --git a/changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml b/changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml
new file mode 100644
index 00000000000..19b633daace
--- /dev/null
+++ b/changelogs/unreleased/43786-on-the-issuable-list-add-tooltips-to-icons.yml
@@ -0,0 +1,5 @@
+---
+title: Add tooltips to icons in lists of issues and merge requests
+merge_request: 17700
+author:
+type: changed
diff --git a/changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml b/changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml
deleted file mode 100644
index 08109632e8e..00000000000
--- a/changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable privileged mode for GitLab Runner
-merge_request: 17528
-author:
-type: added
diff --git a/changelogs/unreleased/43794-fix-domain-verification-validation-errors.yml b/changelogs/unreleased/43794-fix-domain-verification-validation-errors.yml
new file mode 100644
index 00000000000..861820c7538
--- /dev/null
+++ b/changelogs/unreleased/43794-fix-domain-verification-validation-errors.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid validation errors when running the Pages domain verification service
+merge_request: 17992
+author:
+type: fixed
diff --git a/changelogs/unreleased/43802-ensure-foreign-keys-on-clusters-applications.yml b/changelogs/unreleased/43802-ensure-foreign-keys-on-clusters-applications.yml
deleted file mode 100644
index 860a8becd65..00000000000
--- a/changelogs/unreleased/43802-ensure-foreign-keys-on-clusters-applications.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure foreign keys on clusters applications
-merge_request: 17488
-author:
-type: other
diff --git a/changelogs/unreleased/43829-update-ssh-addtion-text.yml b/changelogs/unreleased/43829-update-ssh-addtion-text.yml
deleted file mode 100644
index b7052bb171e..00000000000
--- a/changelogs/unreleased/43829-update-ssh-addtion-text.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update SSH key link to include existing keys
-merge_request:
-author: Brendan O'Leary
-type: changed
diff --git a/changelogs/unreleased/43837-error-handle-in-updating-milestone-on-issue.yml b/changelogs/unreleased/43837-error-handle-in-updating-milestone-on-issue.yml
deleted file mode 100644
index 526523964c3..00000000000
--- a/changelogs/unreleased/43837-error-handle-in-updating-milestone-on-issue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Stop loading spinner on error of milestone update on issue
-merge_request: 17507
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/43924-breadcrumbs-on-project-tags.yml b/changelogs/unreleased/43924-breadcrumbs-on-project-tags.yml
deleted file mode 100644
index 67c223b31c5..00000000000
--- a/changelogs/unreleased/43924-breadcrumbs-on-project-tags.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove extra breadcrumb on tags
-merge_request: 17562
-author: Takuya Noguchi
-type: fixed
diff --git a/changelogs/unreleased/43933-always-notify-mentions.yml b/changelogs/unreleased/43933-always-notify-mentions.yml
new file mode 100644
index 00000000000..7b494d38541
--- /dev/null
+++ b/changelogs/unreleased/43933-always-notify-mentions.yml
@@ -0,0 +1,6 @@
+---
+title: Send @mention notifications even if a user has explicitly unsubscribed from
+ item
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/44022-singular-1-diff.yml b/changelogs/unreleased/44022-singular-1-diff.yml
new file mode 100644
index 00000000000..f4942925a73
--- /dev/null
+++ b/changelogs/unreleased/44022-singular-1-diff.yml
@@ -0,0 +1,5 @@
+---
+title: Use singular in the diff stats if only one line has been changed
+merge_request: 17697
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/44024-fix-table-extra-column.yml b/changelogs/unreleased/44024-fix-table-extra-column.yml
deleted file mode 100644
index 92c354a0844..00000000000
--- a/changelogs/unreleased/44024-fix-table-extra-column.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix markdown table showing extra column
-merge_request: 17669
-author:
-type: fixed
diff --git a/changelogs/unreleased/44149-issue-comment-buttons.yml b/changelogs/unreleased/44149-issue-comment-buttons.yml
deleted file mode 100644
index c874c0d3d66..00000000000
--- a/changelogs/unreleased/44149-issue-comment-buttons.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix broken loading state for close issue button
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml b/changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml
new file mode 100644
index 00000000000..12c73281998
--- /dev/null
+++ b/changelogs/unreleased/44218-add-internationalization-support-for-the-prometheus-merge-request-widget.yml
@@ -0,0 +1,5 @@
+---
+title: Added i18n support for the prometheus memory widget
+merge_request: 17753
+author:
+type: other
diff --git a/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml b/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml
new file mode 100644
index 00000000000..934860b95fe
--- /dev/null
+++ b/changelogs/unreleased/44257-viewing-a-particular-commit-gives-500-error-error-undefined-method-binary.yml
@@ -0,0 +1,5 @@
+---
+title: Fix viewing diffs on old merge requests
+merge_request: 17805
+author:
+type: fixed
diff --git a/changelogs/unreleased/44280-fix-code-search.yml b/changelogs/unreleased/44280-fix-code-search.yml
new file mode 100644
index 00000000000..07f3abb224c
--- /dev/null
+++ b/changelogs/unreleased/44280-fix-code-search.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search results stripping last endline when parsing the results
+merge_request: 17777
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml b/changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml
new file mode 100644
index 00000000000..dd8c0b19d5f
--- /dev/null
+++ b/changelogs/unreleased/44382-ui-breakdown-for-create-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: Fix UI breakdown for Create merge request button
+merge_request: 17821
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/44383-cleanup-framework-header.yml b/changelogs/unreleased/44383-cleanup-framework-header.yml
new file mode 100644
index 00000000000..ef9be9f48de
--- /dev/null
+++ b/changelogs/unreleased/44383-cleanup-framework-header.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up selectors in framework/header.scss
+merge_request: 17822
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml b/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml
new file mode 100644
index 00000000000..79c470ea4e1
--- /dev/null
+++ b/changelogs/unreleased/44384-cleanup-css-for-nested-lists.yml
@@ -0,0 +1,5 @@
+---
+title: Unify format for nested non-task lists
+merge_request: 17823
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/44386-better-ux-for-long-name-branches.yml b/changelogs/unreleased/44386-better-ux-for-long-name-branches.yml
new file mode 100644
index 00000000000..16712486f0f
--- /dev/null
+++ b/changelogs/unreleased/44386-better-ux-for-long-name-branches.yml
@@ -0,0 +1,5 @@
+---
+title: UX re-design branch items with flexbox
+merge_request: 17832
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml b/changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml
new file mode 100644
index 00000000000..c21d02d4d87
--- /dev/null
+++ b/changelogs/unreleased/44388-update-rack-protection-to-2-0-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update rack-protection to 2.0.1
+merge_request: 17835
+author: Takuya Noguchi
+type: security
diff --git a/changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml b/changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml
new file mode 100644
index 00000000000..636fde601ee
--- /dev/null
+++ b/changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml
@@ -0,0 +1,5 @@
+---
+title: Don't capture trailing punctuation when autolinking
+merge_request: 17965
+author:
+type: fixed
diff --git a/changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml b/changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml
new file mode 100644
index 00000000000..5afb1e3d908
--- /dev/null
+++ b/changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml
@@ -0,0 +1,5 @@
+---
+title: 'Cloning a repository over HTTPS with LDAP credentials causes a HTTP 401 Access denied'
+merge_request: !17988
+author: Horatiu Eugen Vlad
+type: fixed
diff --git a/changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml b/changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml
new file mode 100644
index 00000000000..a64b0efa1ed
--- /dev/null
+++ b/changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml
@@ -0,0 +1,5 @@
+---
+title: Fix autolinking URLs containing ampersands
+merge_request: 18045
+author:
+type: fixed
diff --git a/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml b/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml
new file mode 100644
index 00000000000..bdfed89d2ea
--- /dev/null
+++ b/changelogs/unreleased/44712-update-asciidoctor-from-1-5-3-to-1-5-6-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update asciidoctor-plantuml to 0.0.8
+merge_request: 18022
+author: Takuya Noguchi
+type: performance
diff --git a/changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml b/changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml
deleted file mode 100644
index c0fa8e2e377..00000000000
--- a/changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make sure wiki exists when it's enabled
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/4826-geo-wikisyncservice-attempts-to-sync-projects.yml b/changelogs/unreleased/4826-geo-wikisyncservice-attempts-to-sync-projects.yml
deleted file mode 100644
index 7f1ccbfcc7e..00000000000
--- a/changelogs/unreleased/4826-geo-wikisyncservice-attempts-to-sync-projects.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Create empty wiki when import from GitLab and wiki is not there
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/4826-github-import-wiki-fix-1.yml b/changelogs/unreleased/4826-github-import-wiki-fix-1.yml
deleted file mode 100644
index 69145cb6daf..00000000000
--- a/changelogs/unreleased/4826-github-import-wiki-fix-1.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "[GitHub Import] Create an empty wiki if wiki import failed"
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml b/changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml
new file mode 100644
index 00000000000..502c1176d2d
--- /dev/null
+++ b/changelogs/unreleased/ab-43150-users-controller-show-query-limit.yml
@@ -0,0 +1,5 @@
+---
+title: Remove N+1 query for Noteable association.
+merge_request: 17956
+author:
+type: performance
diff --git a/changelogs/unreleased/ab-44467-remove-index.yml b/changelogs/unreleased/ab-44467-remove-index.yml
new file mode 100644
index 00000000000..fb772ce85d5
--- /dev/null
+++ b/changelogs/unreleased/ab-44467-remove-index.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unused index from events table.
+merge_request: 18014
+author:
+type: other
diff --git a/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml b/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml
new file mode 100644
index 00000000000..4db7f76e0af
--- /dev/null
+++ b/changelogs/unreleased/ac-lfs-direct-upload-ee-to-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Port direct upload of LFS artifacts from EE
+merge_request: 17752
+author:
+type: added
diff --git a/changelogs/unreleased/add-indexes-to-todos-for-heavy-users-like-sean.yml b/changelogs/unreleased/add-indexes-to-todos-for-heavy-users-like-sean.yml
deleted file mode 100644
index f0e5103a9d9..00000000000
--- a/changelogs/unreleased/add-indexes-to-todos-for-heavy-users-like-sean.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add partial indexes on todos to handle users with many todos
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/add-query-counts-to-profiler-output.yml b/changelogs/unreleased/add-query-counts-to-profiler-output.yml
new file mode 100644
index 00000000000..8a90b1cbeb0
--- /dev/null
+++ b/changelogs/unreleased/add-query-counts-to-profiler-output.yml
@@ -0,0 +1,5 @@
+---
+title: Add query counts to profiler output
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/ajax-requests-in-performance-bar.yml b/changelogs/unreleased/ajax-requests-in-performance-bar.yml
new file mode 100644
index 00000000000..88cc3678c2b
--- /dev/null
+++ b/changelogs/unreleased/ajax-requests-in-performance-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Allow viewing timings for AJAX requests in the performance bar
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/an-network-controller-fix.yml b/changelogs/unreleased/an-network-controller-fix.yml
deleted file mode 100644
index cb2c447b957..00000000000
--- a/changelogs/unreleased/an-network-controller-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent the graphs page from generating unnecessary Gitaly requests
-merge_request: 37602
-author:
-type: performance
diff --git a/changelogs/unreleased/an-workhorse-3-8-0.yml b/changelogs/unreleased/an-workhorse-3-8-0.yml
deleted file mode 100644
index 5e2a72e1eda..00000000000
--- a/changelogs/unreleased/an-workhorse-3-8-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade Workhorse to version 3.8.0 to support structured logging
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/api-refs-for-commit.yml b/changelogs/unreleased/api-refs-for-commit.yml
deleted file mode 100644
index df8a2b0eccc..00000000000
--- a/changelogs/unreleased/api-refs-for-commit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'API: Get references a commit is pushed to'
-merge_request: 15026
-author: Robert Schilling
-type: added
diff --git a/changelogs/unreleased/asciidoc_inter_document_cross_references.yml b/changelogs/unreleased/asciidoc_inter_document_cross_references.yml
deleted file mode 100644
index 34b26753312..00000000000
--- a/changelogs/unreleased/asciidoc_inter_document_cross_references.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Asciidoc now support inter-document cross references between files in repository
-merge_request: 17125
-author: Turo Soisenniemi
-type: changed
diff --git a/changelogs/unreleased/assignees-vue-component-missing-data-container.yml b/changelogs/unreleased/assignees-vue-component-missing-data-container.yml
deleted file mode 100644
index 233d983b415..00000000000
--- a/changelogs/unreleased/assignees-vue-component-missing-data-container.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add Assignees vue component missing data container
-merge_request: 17426
-author: George Tsiolis
-type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml b/changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml
new file mode 100644
index 00000000000..a9c6fcbf428
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-update-state_machines-activerecord-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Bump `state_machines-activerecord` to 0.5.1
+merge_request: 17924
+author: blackst0ne
+type: other
diff --git a/changelogs/unreleased/bvl-allow-maintainer-to-push.yml b/changelogs/unreleased/bvl-allow-maintainer-to-push.yml
deleted file mode 100644
index a3fefc2889a..00000000000
--- a/changelogs/unreleased/bvl-allow-maintainer-to-push.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow maintainers to push to forks of their projects when a merge request is open
-merge_request: 17395
-author:
-type: added
diff --git a/changelogs/unreleased/bvl-no-permanent-redirect.yml b/changelogs/unreleased/bvl-no-permanent-redirect.yml
new file mode 100644
index 00000000000..c34a3789b58
--- /dev/null
+++ b/changelogs/unreleased/bvl-no-permanent-redirect.yml
@@ -0,0 +1,5 @@
+---
+title: Don't create permanent redirect routes
+merge_request: 17521
+author:
+type: changed
diff --git a/changelogs/unreleased/bvl-port-of-ee-translations.yml b/changelogs/unreleased/bvl-port-of-ee-translations.yml
deleted file mode 100644
index 8f232ec8da3..00000000000
--- a/changelogs/unreleased/bvl-port-of-ee-translations.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Started translation into Turkish, Indonesian and Filipino
-merge_request: 17526
-author:
-type: other
diff --git a/changelogs/unreleased/cache-refactor.yml b/changelogs/unreleased/cache-refactor.yml
deleted file mode 100644
index dec7a0392a5..00000000000
--- a/changelogs/unreleased/cache-refactor.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Cache MergeRequests can_be_resolved_in_ui? git operations
-merge_request: 17589
-author:
-type: performance
diff --git a/changelogs/unreleased/ce-jej-github-project-service-for-ci.yml b/changelogs/unreleased/ce-jej-github-project-service-for-ci.yml
deleted file mode 100644
index 6102b7ecd93..00000000000
--- a/changelogs/unreleased/ce-jej-github-project-service-for-ci.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Hook data for pipelines includes detailed_status
-merge_request: 17607
-author:
-type: changed
diff --git a/changelogs/unreleased/ce-jej-integrations-can-hide-trigger-checkboxes.yml b/changelogs/unreleased/ce-jej-integrations-can-hide-trigger-checkboxes.yml
deleted file mode 100644
index 771df06e7a6..00000000000
--- a/changelogs/unreleased/ce-jej-integrations-can-hide-trigger-checkboxes.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Avoid showing unnecessary Trigger checkboxes for project Integrations with
- only one event
-merge_request: 17607
-author:
-type: changed
diff --git a/changelogs/unreleased/change-strip-whitespace-from-username-input-42637.yml b/changelogs/unreleased/change-strip-whitespace-from-username-input-42637.yml
deleted file mode 100644
index a51781396ee..00000000000
--- a/changelogs/unreleased/change-strip-whitespace-from-username-input-42637.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Remove whitespace from the username/email sign in form field
-merge_request: 17020
-author: Peter lauck
-type: changed
diff --git a/changelogs/unreleased/ci-pipeline-commit-lookup.yml b/changelogs/unreleased/ci-pipeline-commit-lookup.yml
new file mode 100644
index 00000000000..b2a1e4c2163
--- /dev/null
+++ b/changelogs/unreleased/ci-pipeline-commit-lookup.yml
@@ -0,0 +1,5 @@
+---
+title: Use porcelain commit lookup method on CI::CreatePipelineService
+merge_request: 17911
+author:
+type: fixed
diff --git a/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml b/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml
new file mode 100644
index 00000000000..92a03070d78
--- /dev/null
+++ b/changelogs/unreleased/dashboard-view-user-choices-issues-merge-requests.yml
@@ -0,0 +1,5 @@
+---
+title: Add 'Assigned Issues' and 'Assigned Merge Requests' as dashboard view choices for users
+merge_request: 17860
+author: Elias Werberich
+type: added
diff --git a/changelogs/unreleased/discussions-api.yml b/changelogs/unreleased/discussions-api.yml
deleted file mode 100644
index 110df3aa414..00000000000
--- a/changelogs/unreleased/discussions-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add discussions API for Issues and Snippets
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/dm-dont-cache-nil-root-ref.yml b/changelogs/unreleased/dm-dont-cache-nil-root-ref.yml
deleted file mode 100644
index 4dab7d0ffca..00000000000
--- a/changelogs/unreleased/dm-dont-cache-nil-root-ref.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't cache a nil repository root ref to prevent caching issues
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-escape-commit-message.yml b/changelogs/unreleased/dm-escape-commit-message.yml
deleted file mode 100644
index 89af2da3484..00000000000
--- a/changelogs/unreleased/dm-escape-commit-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Escape HTML entities in commit messages
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-go-get-api-token.yml b/changelogs/unreleased/dm-go-get-api-token.yml
deleted file mode 100644
index ad9cfe05849..00000000000
--- a/changelogs/unreleased/dm-go-get-api-token.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow token authentication on go-get request
-merge_request:
-author:
-type: changed
diff --git a/changelogs/unreleased/dm-refs-contains-sha-encoding.yml b/changelogs/unreleased/dm-refs-contains-sha-encoding.yml
new file mode 100644
index 00000000000..cdd9ead5a65
--- /dev/null
+++ b/changelogs/unreleased/dm-refs-contains-sha-encoding.yml
@@ -0,0 +1,5 @@
+---
+title: Fix listing commit branch/tags that contain special characters
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-stuck-import-jobs-verify.yml b/changelogs/unreleased/dm-stuck-import-jobs-verify.yml
deleted file mode 100644
index ed2c2d30f0d..00000000000
--- a/changelogs/unreleased/dm-stuck-import-jobs-verify.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Verify project import status again before marking as failed
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/docs-update-vue-naming-guidelines.yml b/changelogs/unreleased/docs-update-vue-naming-guidelines.yml
deleted file mode 100644
index 95bfd212370..00000000000
--- a/changelogs/unreleased/docs-update-vue-naming-guidelines.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update vue component naming guidelines
-merge_request: 17018
-author: George Tsiolis
-type: other
diff --git a/changelogs/unreleased/dz-improve-app-settings-2.yml b/changelogs/unreleased/dz-improve-app-settings-2.yml
new file mode 100644
index 00000000000..ebe571decb8
--- /dev/null
+++ b/changelogs/unreleased/dz-improve-app-settings-2.yml
@@ -0,0 +1,5 @@
+---
+title: Redesign application settings to match project settings
+merge_request: 18019
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-namespace-id-not-null.yml b/changelogs/unreleased/dz-namespace-id-not-null.yml
deleted file mode 100644
index 07b32aeeb86..00000000000
--- a/changelogs/unreleased/dz-namespace-id-not-null.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add NOT NULL constraint to projects.namespace_id
-merge_request: 17448
-author:
-type: other
diff --git a/changelogs/unreleased/dz-plugins-project-integrations.yml b/changelogs/unreleased/dz-plugins-project-integrations.yml
deleted file mode 100644
index 9dbe82f9af8..00000000000
--- a/changelogs/unreleased/dz-plugins-project-integrations.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add plugins list to the system hooks page
-merge_request: 17518
-author:
-type: added
diff --git a/changelogs/unreleased/dz-system-hooks-plugins.yml b/changelogs/unreleased/dz-system-hooks-plugins.yml
deleted file mode 100644
index e6eb1dfb03b..00000000000
--- a/changelogs/unreleased/dz-system-hooks-plugins.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add ability to use external plugins as an alternative to system hooks
-merge_request: 17003
-author:
-type: added
diff --git a/changelogs/unreleased/ee-4862-verify-file-checksums.yml b/changelogs/unreleased/ee-4862-verify-file-checksums.yml
deleted file mode 100644
index 392c766ab37..00000000000
--- a/changelogs/unreleased/ee-4862-verify-file-checksums.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Foreground verification of uploads and LFS objects
-merge_request: 17402
-author:
-type: added
diff --git a/changelogs/unreleased/expose-commits-mr-api.yml b/changelogs/unreleased/expose-commits-mr-api.yml
new file mode 100644
index 00000000000..77ea2f27431
--- /dev/null
+++ b/changelogs/unreleased/expose-commits-mr-api.yml
@@ -0,0 +1,5 @@
+---
+title: Allow merge requests related to a commit to be found via API
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/feature--2848-display-time-tracking-totals-milestone-page.yml b/changelogs/unreleased/feature--2848-display-time-tracking-totals-milestone-page.yml
deleted file mode 100644
index ca877d32b05..00000000000
--- a/changelogs/unreleased/feature--2848-display-time-tracking-totals-milestone-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: "#28481: Display time tracking totals on milestone page"
-merge_request: 16753
-author: Riccardo Padovani
-type: added
diff --git a/changelogs/unreleased/feature--43691-count-diff-note-calendar-activity.yml b/changelogs/unreleased/feature--43691-count-diff-note-calendar-activity.yml
deleted file mode 100644
index d8020592897..00000000000
--- a/changelogs/unreleased/feature--43691-count-diff-note-calendar-activity.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Count comments on diffs and discussions as contributions for the contributions calendar
-merge_request: 17418
-author: Riccardo Padovani
-type: fixed
diff --git a/changelogs/unreleased/feature-26598-clear-button-ci-lint.yml b/changelogs/unreleased/feature-26598-clear-button-ci-lint.yml
deleted file mode 100644
index fcf237f20f0..00000000000
--- a/changelogs/unreleased/feature-26598-clear-button-ci-lint.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added clear button to ci lint editor
-merge_request:
-author: Michael Robinson
diff --git a/changelogs/unreleased/feature-edit_pages_domain.yml b/changelogs/unreleased/feature-edit_pages_domain.yml
deleted file mode 100644
index bd0af53296c..00000000000
--- a/changelogs/unreleased/feature-edit_pages_domain.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Pages custom domain: allow update of key/certificate'
-merge_request: 17376
-author: rfwatson
-type: changed
diff --git a/changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml b/changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml
deleted file mode 100644
index 28820649af3..00000000000
--- a/changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add catch-up background migration to migrate pipeline stages
-merge_request: 15741
-author:
-type: performance
diff --git a/changelogs/unreleased/feature-include-custom-attributes-in-api.yml b/changelogs/unreleased/feature-include-custom-attributes-in-api.yml
deleted file mode 100644
index f1087d7f7cc..00000000000
--- a/changelogs/unreleased/feature-include-custom-attributes-in-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow including custom attributes in API responses
-merge_request: 16526
-author: Markus Koller
-type: changed
diff --git a/changelogs/unreleased/feature-oidc-groups-claim.yml b/changelogs/unreleased/feature-oidc-groups-claim.yml
deleted file mode 100644
index bde19130114..00000000000
--- a/changelogs/unreleased/feature-oidc-groups-claim.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add groups to OpenID Connect claims
-merge_request: 16929
-author: Hassan Zamani
diff --git a/changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml b/changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml
deleted file mode 100644
index 23a870d6e9f..00000000000
--- a/changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Store sha256 checksum to job artifacts
-merge_request: 17354
-author:
-type: performance
diff --git a/changelogs/unreleased/fix-40798-namespace-forking.yml b/changelogs/unreleased/fix-40798-namespace-forking.yml
new file mode 100644
index 00000000000..095235725f8
--- /dev/null
+++ b/changelogs/unreleased/fix-40798-namespace-forking.yml
@@ -0,0 +1,5 @@
+---
+title: Fix forking to subgroup via API when namespace is given by name
+merge_request: 17815
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/fix-auth0-unsafe-login.yml b/changelogs/unreleased/fix-auth0-unsafe-login.yml
new file mode 100644
index 00000000000..01c6ea69dcc
--- /dev/null
+++ b/changelogs/unreleased/fix-auth0-unsafe-login.yml
@@ -0,0 +1,5 @@
+---
+title: Fix GitLab Auth0 integration signing in the wrong user
+merge_request:
+author:
+type: security
diff --git a/changelogs/unreleased/fix-change-event-body-label-font-size.yml b/changelogs/unreleased/fix-change-event-body-label-font-size.yml
deleted file mode 100644
index 3192a7bff92..00000000000
--- a/changelogs/unreleased/fix-change-event-body-label-font-size.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Apply new default and inline label design
-merge_request: 16956
-author: George Tsiolis
-type: changed
diff --git a/changelogs/unreleased/fix-dropzone-project-show.yml b/changelogs/unreleased/fix-dropzone-project-show.yml
deleted file mode 100644
index 660780812d8..00000000000
--- a/changelogs/unreleased/fix-dropzone-project-show.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix file upload on project show page
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-new-project-path-input-overlapping.yml b/changelogs/unreleased/fix-new-project-path-input-overlapping.yml
deleted file mode 100644
index fb33ce9437a..00000000000
--- a/changelogs/unreleased/fix-new-project-path-input-overlapping.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix new project path input overlapping
-merge_request: 16755
-author: George Tsiolis
-type: fixed
diff --git a/changelogs/unreleased/fix-squash-with-renamed-files.yml b/changelogs/unreleased/fix-squash-with-renamed-files.yml
deleted file mode 100644
index f7cd3a84367..00000000000
--- a/changelogs/unreleased/fix-squash-with-renamed-files.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix squashing when a file is renamed
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-template-project-visibility.yml b/changelogs/unreleased/fix-template-project-visibility.yml
deleted file mode 100644
index 6576097822b..00000000000
--- a/changelogs/unreleased/fix-template-project-visibility.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Respect description and visibility when creating project from template
-merge_request: 16820
-author: George Tsiolis
-type: fixed
diff --git a/changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml b/changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml
new file mode 100644
index 00000000000..7fa6f6a5874
--- /dev/null
+++ b/changelogs/unreleased/fj-15329-services-callbacks-ssrf.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed some SSRF vulnerabilities in services, hooks and integrations
+merge_request: 2337
+author:
+type: security
diff --git a/changelogs/unreleased/fj-28141-redirection-loop.yml b/changelogs/unreleased/fj-28141-redirection-loop.yml
deleted file mode 100644
index db7e109a06e..00000000000
--- a/changelogs/unreleased/fj-28141-redirection-loop.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removing the two factor check when the user sets a new password
-merge_request: 17457
-author:
-type: fixed
diff --git a/changelogs/unreleased/fj-41174-projects-groups-badges-api.yml b/changelogs/unreleased/fj-41174-projects-groups-badges-api.yml
deleted file mode 100644
index 7cb12e26332..00000000000
--- a/changelogs/unreleased/fj-41174-projects-groups-badges-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Implemented badge API endpoints
-merge_request: 17082
-author:
-type: added
diff --git a/changelogs/unreleased/fj-42910-unauthenticated-limit-via-ssh.yml b/changelogs/unreleased/fj-42910-unauthenticated-limit-via-ssh.yml
deleted file mode 100644
index cef339ef787..00000000000
--- a/changelogs/unreleased/fj-42910-unauthenticated-limit-via-ssh.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed bug with unauthenticated requests through git ssh
-merge_request: 17149
-author:
-type: fixed
diff --git a/changelogs/unreleased/fl-refresh-btn.yml b/changelogs/unreleased/fl-refresh-btn.yml
deleted file mode 100644
index 640fdda9ce7..00000000000
--- a/changelogs/unreleased/fl-refresh-btn.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Show loading button inline in refresh button in MR widget
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/group-label-page-breadcrumb.yml b/changelogs/unreleased/group-label-page-breadcrumb.yml
deleted file mode 100644
index c6cc4618c52..00000000000
--- a/changelogs/unreleased/group-label-page-breadcrumb.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix breadcrumb on labels page for groups
-merge_request: 17045
-author: Onuwa Nnachi Isaac
-type: fixed
diff --git a/changelogs/unreleased/ide-folder-button-path.yml b/changelogs/unreleased/ide-folder-button-path.yml
new file mode 100644
index 00000000000..84a122fab75
--- /dev/null
+++ b/changelogs/unreleased/ide-folder-button-path.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed IDE button opening the wrong URL in tree list
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/ide-project-avatar-identicon.yml b/changelogs/unreleased/ide-project-avatar-identicon.yml
new file mode 100644
index 00000000000..2b8b00018a8
--- /dev/null
+++ b/changelogs/unreleased/ide-project-avatar-identicon.yml
@@ -0,0 +1,5 @@
+---
+title: Make project avatar in IDE consistent with the rest of GitLab
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/increase-unicorn-memory-killer-limits.yml b/changelogs/unreleased/increase-unicorn-memory-killer-limits.yml
new file mode 100644
index 00000000000..6d7d2df4f4a
--- /dev/null
+++ b/changelogs/unreleased/increase-unicorn-memory-killer-limits.yml
@@ -0,0 +1,5 @@
+---
+title: Increase the memory limits used in the unicorn killer
+merge_request: 17948
+author:
+type: other
diff --git a/changelogs/unreleased/issue-39885.yml b/changelogs/unreleased/issue-39885.yml
deleted file mode 100644
index 75bf9434152..00000000000
--- a/changelogs/unreleased/issue-39885.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Ensure users cannot create environments with leading or trailing slashes (Fixes #39885)'
-merge_request: 15273
-author:
-type: fixed
diff --git a/changelogs/unreleased/zj-move-opt-out-ruby-endpoints.yml b/changelogs/unreleased/issue_25542.yml
index 0ddb42bc80a..eba491f7e2a 100644
--- a/changelogs/unreleased/zj-move-opt-out-ruby-endpoints.yml
+++ b/changelogs/unreleased/issue_25542.yml
@@ -1,5 +1,5 @@
---
-title: Move Ruby endpoints to OPT_OUT
+title: Improve JIRA event descriptions
merge_request:
author:
type: other
diff --git a/changelogs/unreleased/issue_31081.yml b/changelogs/unreleased/issue_31081.yml
deleted file mode 100644
index ac547c285db..00000000000
--- a/changelogs/unreleased/issue_31081.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use host URL to build JIRA remote link icon
-merge_request:
-author:
-type: other
diff --git a/changelogs/unreleased/issue_38337.yml b/changelogs/unreleased/issue_38337.yml
deleted file mode 100644
index df65118b65c..00000000000
--- a/changelogs/unreleased/issue_38337.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add one group board to Libre
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml b/changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml
deleted file mode 100644
index 6b7e14c6cfc..00000000000
--- a/changelogs/unreleased/jivl-new-modal-project-labels-milestones.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Added new design for promotion modals
-merge_request: 17197
-author:
-type: other
diff --git a/changelogs/unreleased/jprovazn-issueref.yml b/changelogs/unreleased/jprovazn-issueref.yml
new file mode 100644
index 00000000000..ee19cac7b19
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-issueref.yml
@@ -0,0 +1,6 @@
+---
+title: Display state indicator for issuable references in non-project scope (e.g.
+ when referencing issuables from group scope).
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-scoped-limit.yml b/changelogs/unreleased/jprovazn-scoped-limit.yml
deleted file mode 100644
index 45724bb3479..00000000000
--- a/changelogs/unreleased/jprovazn-scoped-limit.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Optimize search queries on the search page by setting a limit for matching
- records in project scope
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/kp-label-select-vue.yml b/changelogs/unreleased/kp-label-select-vue.yml
deleted file mode 100644
index 1f5952f2554..00000000000
--- a/changelogs/unreleased/kp-label-select-vue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Port Labels Select dropdown to Vue
-merge_request: 17411
-author:
-type: other
diff --git a/changelogs/unreleased/merge-requests-api-filter-by-branch.yml b/changelogs/unreleased/merge-requests-api-filter-by-branch.yml
deleted file mode 100644
index 03a7e4d0f71..00000000000
--- a/changelogs/unreleased/merge-requests-api-filter-by-branch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add support for filtering by source and target branch to merge requests API
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/move-email-footer-info-to-single-line.yml b/changelogs/unreleased/move-email-footer-info-to-single-line.yml
new file mode 100644
index 00000000000..87ed5638056
--- /dev/null
+++ b/changelogs/unreleased/move-email-footer-info-to-single-line.yml
@@ -0,0 +1,5 @@
+---
+title: Move email footer info to a single line
+merge_request: 17916
+author:
+type: changed
diff --git a/changelogs/unreleased/mr-commit-optimization.yml b/changelogs/unreleased/mr-commit-optimization.yml
deleted file mode 100644
index 522d8951b18..00000000000
--- a/changelogs/unreleased/mr-commit-optimization.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use persisted/memoized value for MRs shas instead of doing git lookups
-merge_request: 17555
-author:
-type: performance
diff --git a/changelogs/unreleased/oauth_generic_provider.yml b/changelogs/unreleased/oauth_generic_provider.yml
deleted file mode 100644
index 3b6f8b04529..00000000000
--- a/changelogs/unreleased/oauth_generic_provider.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make oauth provider login generic
-merge_request: 8809
-author: Horatiu Eugen Vlad \ No newline at end of file
diff --git a/changelogs/unreleased/optional-api-delimiter.yml b/changelogs/unreleased/optional-api-delimiter.yml
new file mode 100644
index 00000000000..0bcd0787306
--- /dev/null
+++ b/changelogs/unreleased/optional-api-delimiter.yml
@@ -0,0 +1,5 @@
+---
+title: Make /-/ delimiter optional for search endpoints
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/osw-43951-single-batch-blob-request-to-gitaly.yml b/changelogs/unreleased/osw-43951-single-batch-blob-request-to-gitaly.yml
deleted file mode 100644
index 34f834298b6..00000000000
--- a/changelogs/unreleased/osw-43951-single-batch-blob-request-to-gitaly.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Submit a single batch blob RPC to Gitaly per HTTP request when viewing diffs
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/osw-stop-recalculating-merge-base-on-mr-loading.yml b/changelogs/unreleased/osw-stop-recalculating-merge-base-on-mr-loading.yml
deleted file mode 100644
index 1673e1d3658..00000000000
--- a/changelogs/unreleased/osw-stop-recalculating-merge-base-on-mr-loading.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Avoid re-fetching merge-base SHA from Gitaly unnecessarily
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/pages_force_https.yml b/changelogs/unreleased/pages_force_https.yml
new file mode 100644
index 00000000000..da7e29087f3
--- /dev/null
+++ b/changelogs/unreleased/pages_force_https.yml
@@ -0,0 +1,5 @@
+---
+title: Add HTTPS-only pages
+merge_request: 16273
+author: rfwatson
+type: added
diff --git a/changelogs/unreleased/poc-upload-hashing-path.yml b/changelogs/unreleased/poc-upload-hashing-path.yml
new file mode 100644
index 00000000000..7970405bea1
--- /dev/null
+++ b/changelogs/unreleased/poc-upload-hashing-path.yml
@@ -0,0 +1,5 @@
+---
+title: File uploads in remote storage now support project renaming.
+merge_request: 4597
+author:
+type: fixed
diff --git a/changelogs/unreleased/proper-fix-for-artifacts-service.yml b/changelogs/unreleased/proper-fix-for-artifacts-service.yml
deleted file mode 100644
index e92e995dbf5..00000000000
--- a/changelogs/unreleased/proper-fix-for-artifacts-service.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add archive feature to trace
-merge_request: 17314
-author:
-type: added
diff --git a/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml b/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml
new file mode 100644
index 00000000000..1f793fe5e7c
--- /dev/null
+++ b/changelogs/unreleased/reduce-query-count-for-mergerequestscontroller-show.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce number of queries when viewing a merge request
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/refactor-move-assignees-vue-component.yml b/changelogs/unreleased/refactor-move-assignees-vue-component.yml
deleted file mode 100644
index 98cfa6b4c81..00000000000
--- a/changelogs/unreleased/refactor-move-assignees-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move Assignees vue component
-merge_request: 16952
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml b/changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml
deleted file mode 100644
index 20d05530513..00000000000
--- a/changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move BoardNewIssue vue component
-merge_request: 16947
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-filtered-search-vue-component.yml b/changelogs/unreleased/refactor-move-filtered-search-vue-component.yml
deleted file mode 100644
index d65318d7ba1..00000000000
--- a/changelogs/unreleased/refactor-move-filtered-search-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move RecentSearchesDropdownContent vue component
-merge_request: 16951
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-issuable-time-tracker-vue-component.yml b/changelogs/unreleased/refactor-move-issuable-time-tracker-vue-component.yml
deleted file mode 100644
index 5ed06c61817..00000000000
--- a/changelogs/unreleased/refactor-move-issuable-time-tracker-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move IssuableTimeTracker vue component
-merge_request: 16948
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml b/changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml
new file mode 100644
index 00000000000..ac41fe23d3d
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-mr-widget-sha-mismatch-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move ShaMismatch vue component
+merge_request: 17546
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/refactor-move-sidebar-assignee-vue-component.yml b/changelogs/unreleased/refactor-move-sidebar-assignee-vue-component.yml
deleted file mode 100644
index e77b651363e..00000000000
--- a/changelogs/unreleased/refactor-move-sidebar-assignee-vue-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Move SidebarAssignees vue component
-merge_request: 17398
-author: George Tsiolis
-type: performance
diff --git a/changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml b/changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml
new file mode 100644
index 00000000000..88a4b8ec8c1
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-time-tracking-comparison-pane-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move TimeTrackingComparisonPane vue component
+merge_request: 17931
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/refactor-move-time-tracking-vue-components.yml b/changelogs/unreleased/refactor-move-time-tracking-vue-components.yml
new file mode 100644
index 00000000000..8151655250a
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-time-tracking-vue-components.yml
@@ -0,0 +1,5 @@
+---
+title: Move TimeTrackingCollapsedState vue component
+merge_request: 17399
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/remove-unnecessary-validate-project.yml b/changelogs/unreleased/remove-unnecessary-validate-project.yml
deleted file mode 100644
index ebc8da03dd8..00000000000
--- a/changelogs/unreleased/remove-unnecessary-validate-project.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Remove unecessary validate: true from belongs_to :project'
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/replace_redcarpet_with_cmark.yml b/changelogs/unreleased/replace_redcarpet_with_cmark.yml
deleted file mode 100644
index 7ce848b0bbd..00000000000
--- a/changelogs/unreleased/replace_redcarpet_with_cmark.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add CommonMark markdown engine (experimental)
-merge_request: 14835
-author: blackst0ne
-type: added
diff --git a/changelogs/unreleased/sh-cache-column-exists.yml b/changelogs/unreleased/sh-cache-column-exists.yml
deleted file mode 100644
index 8bc648f2b32..00000000000
--- a/changelogs/unreleased/sh-cache-column-exists.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Cache column_exists? for application settings
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-cache-table-exists.yml b/changelogs/unreleased/sh-cache-table-exists.yml
deleted file mode 100644
index 37407b2a005..00000000000
--- a/changelogs/unreleased/sh-cache-table-exists.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Cache table_exists?('application_settings') to reduce repeated schema reloads
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-cleanup-after-git-gc.yml b/changelogs/unreleased/sh-cleanup-after-git-gc.yml
deleted file mode 100644
index 4b652f4d6ce..00000000000
--- a/changelogs/unreleased/sh-cleanup-after-git-gc.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Release libgit2 cache and open file descriptors after `git gc` run
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-dashboard-sort-fix.yml b/changelogs/unreleased/sh-dashboard-sort-fix.yml
deleted file mode 100644
index 6fd252f6707..00000000000
--- a/changelogs/unreleased/sh-dashboard-sort-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix project dashboard showing the wrong timestamps
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-geo-error-500-gpg-commit.yml b/changelogs/unreleased/sh-fix-geo-error-500-gpg-commit.yml
deleted file mode 100644
index 5b4bbe0dc7a..00000000000
--- a/changelogs/unreleased/sh-fix-geo-error-500-gpg-commit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix Error 500 when viewing a commit with a GPG signature in Geo
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml b/changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml
deleted file mode 100644
index 7c7ef39cb75..00000000000
--- a/changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't error out in system hook if user has `nil` datetime columns
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-otp-backup-code-invalidation.yml b/changelogs/unreleased/sh-fix-otp-backup-code-invalidation.yml
deleted file mode 100644
index cedb09c9a7a..00000000000
--- a/changelogs/unreleased/sh-fix-otp-backup-code-invalidation.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure that OTP backup codes are always invalidated
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/sh-make-prune-optional-in-git-fetch.yml b/changelogs/unreleased/sh-make-prune-optional-in-git-fetch.yml
deleted file mode 100644
index e961a23a031..00000000000
--- a/changelogs/unreleased/sh-make-prune-optional-in-git-fetch.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make --prune a configurable parameter in fetching a git remote
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-optimize-admin-projects-page.yml b/changelogs/unreleased/sh-optimize-admin-projects-page.yml
deleted file mode 100644
index 242ea758dab..00000000000
--- a/changelogs/unreleased/sh-optimize-admin-projects-page.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix timeouts loading /admin/projects page
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/tc-api-fix-expose_url.yml b/changelogs/unreleased/tc-api-fix-expose_url.yml
deleted file mode 100644
index c701f64d6bf..00000000000
--- a/changelogs/unreleased/tc-api-fix-expose_url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Ensure the API returns https links when https is configured
-merge_request: 17681
-author:
-type: fixed
diff --git a/changelogs/unreleased/tc-re-add-read-only-banner.yml b/changelogs/unreleased/tc-re-add-read-only-banner.yml
new file mode 100644
index 00000000000..35bcd7e184e
--- /dev/null
+++ b/changelogs/unreleased/tc-re-add-read-only-banner.yml
@@ -0,0 +1,5 @@
+---
+title: Add read-only banner to all pages
+merge_request: 17798
+author:
+type: fixed
diff --git a/changelogs/unreleased/unassign-when-leaving.yml b/changelogs/unreleased/unassign-when-leaving.yml
deleted file mode 100644
index c00a87b1749..00000000000
--- a/changelogs/unreleased/unassign-when-leaving.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't delete todos or unassign issues and MRs when a user leaves a project
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml b/changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml
new file mode 100644
index 00000000000..9c13bfbaf6f
--- /dev/null
+++ b/changelogs/unreleased/update-spec-import-path-for-vue-mount-component-helper.yml
@@ -0,0 +1,5 @@
+---
+title: Update spec import path for vue mount component helper
+merge_request: 17880
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/update-unresolved-discussions-vue-component.yml b/changelogs/unreleased/update-unresolved-discussions-vue-component.yml
new file mode 100644
index 00000000000..246eaaae2bd
--- /dev/null
+++ b/changelogs/unreleased/update-unresolved-discussions-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Add i18n and update specs for ShaMismatch vue component
+merge_request: 17870
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/upgrade-workhorse-4-0-0.yml b/changelogs/unreleased/upgrade-workhorse-4-0-0.yml
deleted file mode 100644
index f9dbdc7fc56..00000000000
--- a/changelogs/unreleased/upgrade-workhorse-4-0-0.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade GitLab Workhorse to 4.0.0
-merge_request:
-author:
-type: added
diff --git a/changelogs/unreleased/winh-deprecate-old-modal.yml b/changelogs/unreleased/winh-deprecate-old-modal.yml
new file mode 100644
index 00000000000..4fae1fafbea
--- /dev/null
+++ b/changelogs/unreleased/winh-deprecate-old-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Rename modal.vue to deprecated_modal.vue
+merge_request: 17438
+author:
+type: other
diff --git a/changelogs/unreleased/winh-new-modal-component.yml b/changelogs/unreleased/winh-new-modal-component.yml
deleted file mode 100644
index bcc0d489c88..00000000000
--- a/changelogs/unreleased/winh-new-modal-component.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add new modal Vue component
-merge_request: 17108
-author:
-type: changed
diff --git a/changelogs/unreleased/wip-new-mr-cmd.yml b/changelogs/unreleased/wip-new-mr-cmd.yml
deleted file mode 100644
index e930758ec9d..00000000000
--- a/changelogs/unreleased/wip-new-mr-cmd.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Port /wip quick action command to Merge Request creation (on description)
-merge_request: 17463
-author: Adam Pahlevi
-type: added
diff --git a/changelogs/unreleased/workhorse-gitaly-mandatory.yml b/changelogs/unreleased/workhorse-gitaly-mandatory.yml
new file mode 100644
index 00000000000..77b62302e86
--- /dev/null
+++ b/changelogs/unreleased/workhorse-gitaly-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Make all workhorse gitaly calls opt-out, take 2
+merge_request: 18043
+author:
+type: other
diff --git a/changelogs/unreleased/zj-remote-repo-exists.yml b/changelogs/unreleased/zj-remote-repo-exists.yml
new file mode 100644
index 00000000000..f024b83159b
--- /dev/null
+++ b/changelogs/unreleased/zj-remote-repo-exists.yml
@@ -0,0 +1,5 @@
+---
+title: Test if remote repository exists when importing wikis
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/zj-version-string-grouping-ci.yml b/changelogs/unreleased/zj-version-string-grouping-ci.yml
deleted file mode 100644
index 04ef0f65b1e..00000000000
--- a/changelogs/unreleased/zj-version-string-grouping-ci.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow CI/CD Jobs being grouped on version strings
-merge_request:
-author:
-type: added
diff --git a/config.ru b/config.ru
index 7b15939c6ff..405d01863ac 100644
--- a/config.ru
+++ b/config.ru
@@ -7,8 +7,8 @@ if defined?(Unicorn)
# Unicorn self-process killer
require 'unicorn/worker_killer'
- min = (ENV['GITLAB_UNICORN_MEMORY_MIN'] || 300 * 1 << 20).to_i
- max = (ENV['GITLAB_UNICORN_MEMORY_MAX'] || 350 * 1 << 20).to_i
+ min = (ENV['GITLAB_UNICORN_MEMORY_MIN'] || 400 * 1 << 20).to_i
+ max = (ENV['GITLAB_UNICORN_MEMORY_MAX'] || 650 * 1 << 20).to_i
# Max memory size (RSS) per worker
use Unicorn::WorkerKiller::Oom, min, max
diff --git a/config/application.rb b/config/application.rb
index 0ff95e33a9c..13501d4bdb5 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -170,7 +170,7 @@ module Gitlab
ENV['GIT_TERMINAL_PROMPT'] = '0'
# Gitlab Read-only middleware support
- config.middleware.insert_after ActionDispatch::Flash, 'Gitlab::Middleware::ReadOnly'
+ config.middleware.insert_after ActionDispatch::Flash, '::Gitlab::Middleware::ReadOnly'
config.generators do |g|
g.factory_bot false
diff --git a/config/boot.rb b/config/boot.rb
index f2830ae3166..84f390f3228 100644
--- a/config/boot.rb
+++ b/config/boot.rb
@@ -1,6 +1,11 @@
-require 'rubygems'
+def rails5?
+ %w[1 true].include?(ENV["RAILS5"])
+end
-# Set up gems listed in the Gemfile.
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+require 'rubygems' unless rails5?
+
+gemfile = rails5? ? "Gemfile.rails5" : "Gemfile"
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../#{gemfile}", __dir__)
+# Set up gems listed in the Gemfile.
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
diff --git a/config/environment.rb b/config/environment.rb
index df3006d349c..487a4564b47 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -1,5 +1,11 @@
# Load the rails application
-require File.expand_path('../application', __FILE__)
+
+# Remove this condition when upgraded to rails 5.0.
+if %w[1 true].include?(ENV["RAILS5"])
+ require_relative 'application'
+else
+ require File.expand_path('../application', __FILE__)
+end
# Initialize the rails application
Rails.application.initialize!
diff --git a/config/environments/production.rb b/config/environments/production.rb
index c5cbfcf64cf..9941987929c 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -9,7 +9,11 @@ Rails.application.configure do
config.action_controller.perform_caching = true
# Disable Rails's static asset server (Apache or nginx will already do this)
- config.serve_static_files = false
+ if Gitlab.rails5?
+ config.public_file_server.enabled = false
+ else
+ config.serve_static_files = false
+ end
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
diff --git a/config/environments/test.rb b/config/environments/test.rb
index d09e51e766a..1849c984351 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -18,7 +18,13 @@ Rails.application.configure do
# Configure static asset server for tests with Cache-Control for performance
config.assets.compile = false if ENV['CI']
- config.serve_static_files = true
+
+ if Gitlab.rails5?
+ config.public_file_server.enabled = true
+ else
+ config.serve_static_files = true
+ end
+
config.static_cache_control = "public, max-age=3600"
# Show full error reports and disable caching
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index bd696a7f2c5..8db66037d61 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -145,18 +145,55 @@ production: &base
enabled: true
# The location where build artifacts are stored (default: shared/artifacts).
# path: shared/artifacts
+ # object_store:
+ # enabled: false
+ # remote_directory: artifacts # The bucket name
+ # background_upload: false # Temporary option to limit automatic upload (Default: true)
+ # proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
+ # connection:
+ # provider: AWS # Only AWS supported at the moment
+ # aws_access_key_id: AWS_ACCESS_KEY_ID
+ # aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ # region: eu-central-1
## Git LFS
lfs:
enabled: true
# The location where LFS objects are stored (default: shared/lfs-objects).
# storage_path: shared/lfs-objects
+ object_store:
+ enabled: false
+ remote_directory: lfs-objects # Bucket name
+ # background_upload: false # Temporary option to limit automatic upload (Default: true)
+ # proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
+ connection:
+ provider: AWS
+ aws_access_key_id: AWS_ACCESS_KEY_ID
+ aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ region: eu-central-1
+ # Use the following options to configure an AWS compatible host
+ # host: 'localhost' # default: s3.amazonaws.com
+ # endpoint: 'http://127.0.0.1:9000' # default: nil
+ # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## Uploads (attachments, avatars, etc...)
uploads:
# The location where uploads objects are stored (default: public/).
# storage_path: public/
# base_dir: uploads/-/system
+ object_store:
+ enabled: false
+ # remote_directory: uploads # Bucket name
+ # background_upload: false # Temporary option to limit automatic upload (Default: true)
+ # proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage
+ # connection:
+ # provider: AWS
+ # aws_access_key_id: AWS_ACCESS_KEY_ID
+ # aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ # region: eu-central-1
+ # host: 'localhost' # default: s3.amazonaws.com
+ # endpoint: 'http://127.0.0.1:9000' # default: nil
+ # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## GitLab Pages
pages:
@@ -481,7 +518,17 @@ production: &base
# - { name: 'twitter',
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET' }
- #
+ # - { name: 'jwt',
+ # app_secret: 'YOUR_APP_SECRET',
+ # args: {
+ # algorithm: 'HS256',
+ # uid_claim: 'email',
+ # required_claims: ["name", "email"],
+ # info_map: { name: "name", email: "email" },
+ # auth_url: 'https://example.com/',
+ # valid_within: nil,
+ # }
+ # }
# - { name: 'saml',
# label: 'Our SAML Provider',
# groups_attribute: 'Groups',
@@ -655,10 +702,39 @@ test:
enabled: true
lfs:
enabled: false
+ # The location where LFS objects are stored (default: shared/lfs-objects).
+ # storage_path: shared/lfs-objects
+ object_store:
+ enabled: false
+ remote_directory: lfs-objects # The bucket name
+ connection:
+ provider: AWS # Only AWS supported at the moment
+ aws_access_key_id: AWS_ACCESS_KEY_ID
+ aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ region: us-east-1
artifacts:
path: tmp/tests/artifacts
+ enabled: true
+ # The location where build artifacts are stored (default: shared/artifacts).
+ # path: shared/artifacts
+ object_store:
+ enabled: false
+ remote_directory: artifacts # The bucket name
+ background_upload: false
+ connection:
+ provider: AWS # Only AWS supported at the moment
+ aws_access_key_id: AWS_ACCESS_KEY_ID
+ aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ region: us-east-1
uploads:
storage_path: tmp/tests/public
+ object_store:
+ enabled: false
+ connection:
+ provider: AWS # Only AWS supported at the moment
+ aws_access_key_id: AWS_ACCESS_KEY_ID
+ aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ region: us-east-1
gitlab:
host: localhost
port: 80
@@ -733,6 +809,17 @@ test:
- { name: 'twitter',
app_id: 'YOUR_APP_ID',
app_secret: 'YOUR_APP_SECRET' }
+ - { name: 'jwt',
+ app_secret: 'YOUR_APP_SECRET',
+ args: {
+ algorithm: 'HS256',
+ uid_claim: 'email',
+ required_claims: ["name", "email"],
+ info_map: { name: "name", email: "email" },
+ auth_url: 'https://example.com/',
+ valid_within: nil,
+ }
+ }
- { name: 'auth0',
args: {
client_id: 'YOUR_AUTH0_CLIENT_ID',
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index ea0dee7af53..69b59b26d8c 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -305,6 +305,13 @@ Settings.artifacts['storage_path'] = Settings.absolute(Settings.artifacts.values
# Settings.artifact['path'] is deprecated, use `storage_path` instead
Settings.artifacts['path'] = Settings.artifacts['storage_path']
Settings.artifacts['max_size'] ||= 100 # in megabytes
+Settings.artifacts['object_store'] ||= Settingslogic.new({})
+Settings.artifacts['object_store']['enabled'] = false if Settings.artifacts['object_store']['enabled'].nil?
+Settings.artifacts['object_store']['remote_directory'] ||= nil
+Settings.artifacts['object_store']['background_upload'] = true if Settings.artifacts['object_store']['background_upload'].nil?
+Settings.artifacts['object_store']['proxy_download'] = false if Settings.artifacts['object_store']['proxy_download'].nil?
+# Convert upload connection settings to use string keys, to make Fog happy
+Settings.artifacts['object_store']['connection']&.deep_stringify_keys!
#
# Registry
@@ -340,6 +347,14 @@ Settings.pages['artifacts_server'] ||= Settings.pages['enabled'] if Settings.pa
Settings['lfs'] ||= Settingslogic.new({})
Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
Settings.lfs['storage_path'] = Settings.absolute(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"))
+Settings.lfs['object_store'] ||= Settingslogic.new({})
+Settings.lfs['object_store']['enabled'] = false if Settings.lfs['object_store']['enabled'].nil?
+Settings.lfs['object_store']['remote_directory'] ||= nil
+Settings.lfs['object_store']['direct_upload'] = false if Settings.lfs['object_store']['direct_upload'].nil?
+Settings.lfs['object_store']['background_upload'] = true if Settings.lfs['object_store']['background_upload'].nil?
+Settings.lfs['object_store']['proxy_download'] = false if Settings.lfs['object_store']['proxy_download'].nil?
+# Convert upload connection settings to use string keys, to make Fog happy
+Settings.lfs['object_store']['connection']&.deep_stringify_keys!
#
# Uploads
@@ -347,6 +362,13 @@ Settings.lfs['storage_path'] = Settings.absolute(Settings.lfs['storage_path'] ||
Settings['uploads'] ||= Settingslogic.new({})
Settings.uploads['storage_path'] = Settings.absolute(Settings.uploads['storage_path'] || 'public')
Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system'
+Settings.uploads['object_store'] ||= Settingslogic.new({})
+Settings.uploads['object_store']['enabled'] = false if Settings.uploads['object_store']['enabled'].nil?
+Settings.uploads['object_store']['remote_directory'] ||= 'uploads'
+Settings.uploads['object_store']['background_upload'] = true if Settings.uploads['object_store']['background_upload'].nil?
+Settings.uploads['object_store']['proxy_download'] = false if Settings.uploads['object_store']['proxy_download'].nil?
+# Convert upload connection settings to use string keys, to make Fog happy
+Settings.uploads['object_store']['connection']&.deep_stringify_keys!
#
# Mattermost
@@ -467,12 +489,7 @@ unless Settings.repositories.storages['default']
end
Settings.repositories.storages.each do |key, storage|
- storage = Settingslogic.new(storage)
-
- # Expand relative paths
- storage['path'] = Settings.absolute(storage['path'])
-
- Settings.repositories.storages[key] = storage
+ Settings.repositories.storages[key] = Gitlab::GitalyClient::StorageSettings.new(storage)
end
#
@@ -486,7 +503,7 @@ repositories_storages = Settings.repositories.storages.values
repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(%r{/$}, '')
repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home'])
-if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs['path'].gsub(%r{/$}, '')) }
+if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs.legacy_disk_path.gsub(%r{/$}, '')) }
Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive')
end
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index f8e67ce04c9..d92cdb97766 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -5,7 +5,7 @@ end
def find_parent_path(name, path)
parent = Pathname.new(path).realpath.parent
Gitlab.config.repositories.storages.detect do |n, rs|
- name != n && Pathname.new(rs['path']).realpath == parent
+ name != n && Pathname.new(rs.legacy_disk_path).realpath == parent
end
rescue Errno::EIO, Errno::ENOENT => e
warning = "WARNING: couldn't verify #{path} (#{name}). "\
@@ -33,7 +33,7 @@ def validate_storages_config
"If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n"
end
- if !repository_storage.is_a?(Hash) || repository_storage['path'].nil?
+ if !repository_storage.is_a?(Gitlab::GitalyClient::StorageSettings) || repository_storage.legacy_disk_path.nil?
storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example")
end
@@ -50,7 +50,7 @@ end
def validate_storages_paths
Gitlab.config.repositories.storages.each do |name, repository_storage|
- parent_name, _parent_path = find_parent_path(name, repository_storage['path'])
+ parent_name, _parent_path = find_parent_path(name, repository_storage.legacy_disk_path)
if parent_name
storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages")
end
diff --git a/config/initializers/active_record_locking.rb b/config/initializers/active_record_locking.rb
index 150aaa2a8c2..3e7111fd063 100644
--- a/config/initializers/active_record_locking.rb
+++ b/config/initializers/active_record_locking.rb
@@ -1,73 +1,77 @@
# rubocop:disable Lint/RescueException
-# This patch fixes https://github.com/rails/rails/issues/26024
-# TODO: Remove it when it's no longer necessary
-
-module ActiveRecord
- module Locking
- module Optimistic
- # We overwrite this method because we don't want to have default value
- # for newly created records
- def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
- super
- end
+# Remove this entire initializer when we are at rails 5.0.
+# This file fixes the bug (see below) which has been fixed in the upstream.
+unless Gitlab.rails5?
+ # This patch fixes https://github.com/rails/rails/issues/26024
+ # TODO: Remove it when it's no longer necessary
+
+ module ActiveRecord
+ module Locking
+ module Optimistic
+ # We overwrite this method because we don't want to have default value
+ # for newly created records
+ def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
+ super
+ end
- def _update_record(attribute_names = self.attribute_names) #:nodoc:
- return super unless locking_enabled?
- return 0 if attribute_names.empty?
+ def _update_record(attribute_names = self.attribute_names) #:nodoc:
+ return super unless locking_enabled?
+ return 0 if attribute_names.empty?
- lock_col = self.class.locking_column
+ lock_col = self.class.locking_column
- previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend
+ previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend
- # This line is added as a patch
- previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
+ # This line is added as a patch
+ previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
- increment_lock
+ increment_lock
- attribute_names += [lock_col]
- attribute_names.uniq!
+ attribute_names += [lock_col]
+ attribute_names.uniq!
- begin
- relation = self.class.unscoped
+ begin
+ relation = self.class.unscoped
- affected_rows = relation.where(
- self.class.primary_key => id,
- lock_col => previous_lock_value
- ).update_all(
- attributes_for_update(attribute_names).map do |name|
- [name, _read_attribute(name)]
- end.to_h
- )
+ affected_rows = relation.where(
+ self.class.primary_key => id,
+ lock_col => previous_lock_value
+ ).update_all(
+ attributes_for_update(attribute_names).map do |name|
+ [name, _read_attribute(name)]
+ end.to_h
+ )
- unless affected_rows == 1
- raise ActiveRecord::StaleObjectError.new(self, "update")
- end
+ unless affected_rows == 1
+ raise ActiveRecord::StaleObjectError.new(self, "update")
+ end
- affected_rows
+ affected_rows
- # If something went wrong, revert the version.
- rescue Exception
- send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend
- raise
+ # If something went wrong, revert the version.
+ rescue Exception
+ send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend
+ raise
+ end
end
- end
- # This is patched because we need it to query `lock_version IS NULL`
- # rather than `lock_version = 0` whenever lock_version is NULL.
- def relation_for_destroy
- return super unless locking_enabled?
+ # This is patched because we need it to query `lock_version IS NULL`
+ # rather than `lock_version = 0` whenever lock_version is NULL.
+ def relation_for_destroy
+ return super unless locking_enabled?
- column_name = self.class.locking_column
- super.where(self.class.arel_table[column_name].eq(self[column_name]))
+ column_name = self.class.locking_column
+ super.where(self.class.arel_table[column_name].eq(self[column_name]))
+ end
end
- end
- # This is patched because we want `lock_version` default to `NULL`
- # rather than `0`
- class LockingType < SimpleDelegator
- def type_cast_from_database(value)
- super
+ # This is patched because we want `lock_version` default to `NULL`
+ # rather than `0`
+ class LockingType < SimpleDelegator
+ def type_cast_from_database(value)
+ super
+ end
end
end
end
diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb
new file mode 100644
index 00000000000..a65f8aecf9e
--- /dev/null
+++ b/config/initializers/application_controller_renderer.rb
@@ -0,0 +1,12 @@
+# Remove this `if` condition when upgraded to rails 5.0.
+# The body must be kept.
+if Gitlab.rails5?
+ # Be sure to restart your server when you modify this file.
+
+ # ActiveSupport::Reloader.to_prepare do
+ # ApplicationController.renderer.defaults.merge!(
+ # http_host: 'example.org',
+ # https: false
+ # )
+ # end
+end
diff --git a/config/initializers/ar5_batching.rb b/config/initializers/ar5_batching.rb
index 6ebaf8834d2..874455ce5af 100644
--- a/config/initializers/ar5_batching.rb
+++ b/config/initializers/ar5_batching.rb
@@ -1,41 +1,39 @@
-# Port ActiveRecord::Relation#in_batches from ActiveRecord 5.
-# https://github.com/rails/rails/blob/ac027338e4a165273607dccee49a3d38bc836794/activerecord/lib/active_record/relation/batches.rb#L184
-# TODO: this can be removed once we're using AR5.
-raise "Vendored ActiveRecord 5 code! Delete #{__FILE__}!" if ActiveRecord::VERSION::MAJOR >= 5
-
-module ActiveRecord
- module Batches
- # Differences from upstream: enumerator support was removed, and custom
- # order/limit clauses are ignored without a warning.
- def in_batches(of: 1000, start: nil, finish: nil, load: false)
- raise "Must provide a block" unless block_given?
-
- relation = self.reorder(batch_order).limit(of)
- relation = relation.where(arel_table[primary_key].gteq(start)) if start
- relation = relation.where(arel_table[primary_key].lteq(finish)) if finish
- batch_relation = relation
-
- loop do
- if load
- records = batch_relation.records
- ids = records.map(&:id)
- yielded_relation = self.where(primary_key => ids)
- yielded_relation.load_records(records)
- else
- ids = batch_relation.pluck(primary_key)
- yielded_relation = self.where(primary_key => ids)
+# Remove this file when upgraded to rails 5.0.
+unless Gitlab.rails5?
+ module ActiveRecord
+ module Batches
+ # Differences from upstream: enumerator support was removed, and custom
+ # order/limit clauses are ignored without a warning.
+ def in_batches(of: 1000, start: nil, finish: nil, load: false)
+ raise "Must provide a block" unless block_given?
+
+ relation = self.reorder(batch_order).limit(of)
+ relation = relation.where(arel_table[primary_key].gteq(start)) if start
+ relation = relation.where(arel_table[primary_key].lteq(finish)) if finish
+ batch_relation = relation
+
+ loop do
+ if load
+ records = batch_relation.records
+ ids = records.map(&:id)
+ yielded_relation = self.where(primary_key => ids)
+ yielded_relation.load_records(records)
+ else
+ ids = batch_relation.pluck(primary_key)
+ yielded_relation = self.where(primary_key => ids)
+ end
+
+ break if ids.empty?
+
+ primary_key_offset = ids.last
+ raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
+
+ yield yielded_relation
+
+ break if ids.length < of
+
+ batch_relation = relation.where(arel_table[primary_key].gt(primary_key_offset))
end
-
- break if ids.empty?
-
- primary_key_offset = ids.last
- raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
-
- yield yielded_relation
-
- break if ids.length < of
-
- batch_relation = relation.where(arel_table[primary_key].gt(primary_key_offset))
end
end
end
diff --git a/config/initializers/ar5_pg_10_support.rb b/config/initializers/ar5_pg_10_support.rb
index a529c74a8ce..40548290ce8 100644
--- a/config/initializers/ar5_pg_10_support.rb
+++ b/config/initializers/ar5_pg_10_support.rb
@@ -1,6 +1,5 @@
-raise "Vendored ActiveRecord 5 code! Delete #{__FILE__}!" if ActiveRecord::VERSION::MAJOR >= 5
-
-if Gitlab::Database.postgresql?
+# Remove this file when upgraded to rails 5.0.
+if !Gitlab.rails5? && Gitlab::Database.postgresql?
require 'active_record/connection_adapters/postgresql_adapter'
require 'active_record/connection_adapters/postgresql/schema_statements'
diff --git a/config/initializers/ar_native_database_types.rb b/config/initializers/ar_native_database_types.rb
new file mode 100644
index 00000000000..3522b1db536
--- /dev/null
+++ b/config/initializers/ar_native_database_types.rb
@@ -0,0 +1,11 @@
+require 'active_record/connection_adapters/abstract_mysql_adapter'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractMysqlAdapter
+ NATIVE_DATABASE_TYPES.merge!(
+ bigserial: { name: 'bigint(20) auto_increment PRIMARY KEY' }
+ )
+ end
+ end
+end
diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb
index cd7df44351a..5cde6cbb0ff 100644
--- a/config/initializers/carrierwave.rb
+++ b/config/initializers/carrierwave.rb
@@ -28,16 +28,4 @@ if File.exist?(aws_file)
# when fog_public is false and provider is AWS or Google, defaults to 600
config.fog_authenticated_url_expiration = 1 << 29
end
-
- # Mocking Fog requests, based on: https://github.com/carrierwaveuploader/carrierwave/wiki/How-to%3A-Test-Fog-based-uploaders
- if Rails.env.test?
- Fog.mock!
- connection = ::Fog::Storage.new(
- aws_access_key_id: AWS_CONFIG['access_key_id'],
- aws_secret_access_key: AWS_CONFIG['secret_access_key'],
- provider: 'AWS',
- region: AWS_CONFIG['region']
- )
- connection.directories.create(key: AWS_CONFIG['bucket'])
- end
end
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index f642e6d47e0..362b9cc9a88 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -219,49 +219,5 @@ Devise.setup do |config|
end
end
- Gitlab.config.omniauth.providers.each do |provider|
- provider_arguments = []
-
- %w[app_id app_secret].each do |argument|
- provider_arguments << provider[argument] if provider[argument]
- end
-
- case provider['args']
- when Array
- # An Array from the configuration will be expanded.
- provider_arguments.concat provider['args']
- when Hash
- # Add procs for handling SLO
- if provider['name'] == 'cas3'
- provider['args'][:on_single_sign_out] = lambda do |request|
- ticket = request.params[:session_index]
- raise "Service Ticket not found." unless Gitlab::Auth::OAuth::Session.valid?(:cas3, ticket)
-
- Gitlab::Auth::OAuth::Session.destroy(:cas3, ticket)
- true
- end
- end
-
- if provider['name'] == 'authentiq'
- provider['args'][:remote_sign_out_handler] = lambda do |request|
- authentiq_session = request.params['sid']
- if Gitlab::Auth::OAuth::Session.valid?(:authentiq, authentiq_session)
- Gitlab::Auth::OAuth::Session.destroy(:authentiq, authentiq_session)
- true
- else
- false
- end
- end
- end
-
- if provider['name'] == 'shibboleth'
- provider['args'][:fail_with_empty_uid] = true
- end
-
- # A Hash from the configuration will be passed as is.
- provider_arguments << provider['args'].symbolize_keys
- end
-
- config.omniauth provider['name'].to_sym, *provider_arguments
- end
+ Gitlab::OmniauthInitializer.new(config).execute(Gitlab.config.omniauth.providers)
end
diff --git a/config/initializers/fog_google_https_private_urls.rb b/config/initializers/fog_google_https_private_urls.rb
new file mode 100644
index 00000000000..f92e623a5d2
--- /dev/null
+++ b/config/initializers/fog_google_https_private_urls.rb
@@ -0,0 +1,20 @@
+#
+# Monkey patching the https support for private urls
+# See https://gitlab.com/gitlab-org/gitlab-ee/issues/4879
+#
+module Fog
+ module Storage
+ class GoogleXML
+ class File < Fog::Model
+ module MonkeyPatch
+ def url(expires)
+ requires :key
+ collection.get_https_url(key, expires)
+ end
+ end
+
+ prepend MonkeyPatch
+ end
+ end
+ end
+end
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 114c1cb512f..49fdd23064c 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -1,3 +1,21 @@
+# Monkey patch lograge until https://github.com/roidrage/lograge/pull/241 is released
+module Lograge
+ class RequestLogSubscriber < ActiveSupport::LogSubscriber
+ def strip_query_string(path)
+ index = path.index('?')
+ index ? path[0, index] : path
+ end
+
+ def extract_location
+ location = Thread.current[:lograge_location]
+ return {} unless location
+
+ Thread.current[:lograge_location] = nil
+ { location: strip_query_string(location) }
+ end
+ end
+end
+
# Only use Lograge for Rails
unless Sidekiq.server?
filename = File.join(Rails.root, 'log', "#{Rails.env}_json.log")
diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb
new file mode 100644
index 00000000000..2d130bc0bf8
--- /dev/null
+++ b/config/initializers/new_framework_defaults.rb
@@ -0,0 +1,29 @@
+# Remove this `if` condition when upgraded to rails 5.0.
+# The body must be kept.
+if Gitlab.rails5?
+ # Be sure to restart your server when you modify this file.
+ #
+ # This file contains migration options to ease your Rails 5.0 upgrade.
+ #
+ # Once upgraded flip defaults one by one to migrate to the new default.
+ #
+ # Read the Guide for Upgrading Ruby on Rails for more info on each option.
+
+ Rails.application.config.action_controller.raise_on_unfiltered_parameters = true
+
+ # Enable per-form CSRF tokens. Previous versions had false.
+ Rails.application.config.action_controller.per_form_csrf_tokens = false
+
+ # Enable origin-checking CSRF mitigation. Previous versions had false.
+ Rails.application.config.action_controller.forgery_protection_origin_check = false
+
+ # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
+ # Previous versions had false.
+ ActiveSupport.to_time_preserves_timezone = false
+
+ # Require `belongs_to` associations by default. Previous versions had false.
+ Rails.application.config.active_record.belongs_to_required_by_default = false
+
+ # Do not halt callback chains when a callback returns false. Previous versions had true.
+ ActiveSupport.halt_callback_chains_on_return_false = true
+end
diff --git a/config/karma.config.js b/config/karma.config.js
index 3d95e1622b2..7ede745b591 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -5,7 +5,7 @@ var ROOT_PATH = path.resolve(__dirname, '..');
// remove problematic plugins
if (webpackConfig.plugins) {
- webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin) {
+ webpackConfig.plugins = webpackConfig.plugins.filter(function(plugin) {
return !(
plugin instanceof webpack.optimize.CommonsChunkPlugin ||
plugin instanceof webpack.optimize.ModuleConcatenationPlugin ||
@@ -24,7 +24,7 @@ module.exports = function(config) {
var karmaConfig = {
basePath: ROOT_PATH,
- browsers: ['ChromeHeadlessCustom'],
+ browsers: ['ChromeHeadlessCustom'],
customLaunchers: {
ChromeHeadlessCustom: {
base: 'ChromeHeadless',
@@ -34,7 +34,7 @@ module.exports = function(config) {
// escalated kernel privileges (e.g. docker run --cap-add=CAP_SYS_ADMIN)
'--no-sandbox',
],
- }
+ },
},
frameworks: ['jasmine'],
files: [
@@ -55,7 +55,7 @@ module.exports = function(config) {
reports: ['html', 'text-summary'],
dir: 'coverage-javascript/',
subdir: '.',
- fixWebpackSourcePaths: true
+ fixWebpackSourcePaths: true,
};
karmaConfig.browserNoActivityTimeout = 60000; // 60 seconds
}
diff --git a/config/routes.rb b/config/routes.rb
index 35fd76fb119..52726f94753 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -44,7 +44,7 @@ Rails.application.routes.draw do
get 'readiness' => 'health#readiness'
post 'storage_check' => 'health#storage_check'
resources :metrics, only: [:index]
- mount Peek::Railtie => '/peek'
+ mount Peek::Railtie => '/peek', as: 'peek_routes'
# Boards resources shared between group and projects
resources :boards, only: [] do
@@ -61,6 +61,9 @@ Rails.application.routes.draw do
# UserCallouts
resources :user_callouts, only: [:create]
+
+ get 'ide' => 'ide#index'
+ get 'ide/*vueroute' => 'ide#index', format: false
end
# Koding route
diff --git a/config/routes/ci.rb b/config/routes/ci.rb
index 60c1724bc05..ebd321ed097 100644
--- a/config/routes/ci.rb
+++ b/config/routes/ci.rb
@@ -1,5 +1,5 @@
namespace :ci do
- resource :lint, only: [:show, :create]
+ resource :lint, only: :show
root to: redirect('')
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index c803737d40b..48ba8ef06f9 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -52,7 +52,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resource :pages, only: [:show, :destroy] do
+ resource :pages, only: [:show, :update, :destroy] do
resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do
member do
post :verify
@@ -280,6 +280,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :keep
end
end
+
+ namespace :ci do
+ resource :lint, only: [:show, :create]
+ end
end
draw :legacy_builds
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 554502c5d83..c811034b29d 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -68,5 +68,7 @@
- [project_migrate_hashed_storage, 1]
- [storage_migrator, 1]
- [pages_domain_verification, 1]
+ - [object_storage_upload, 1]
+ - [object_storage, 1]
- [plugin, 1]
- [pipeline_background, 1]
diff --git a/config/spring.rb b/config/spring.rb
new file mode 100644
index 00000000000..c9119b40c08
--- /dev/null
+++ b/config/spring.rb
@@ -0,0 +1,6 @@
+%w(
+ .ruby-version
+ .rbenv-vars
+ tmp/restart.txt
+ tmp/caching-dev.txt
+).each { |path| Spring.watch(path) }
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 3403c0c207d..b74d9dde494 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -1,5 +1,3 @@
-'use strict';
-
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
@@ -27,10 +25,10 @@ let watchAutoEntries = [];
function generateEntries() {
// generate automatic entry points
const autoEntries = {};
- const pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
- watchAutoEntries = [
- path.join(ROOT_PATH, 'app/assets/javascripts/pages/'),
- ];
+ const pageEntries = glob.sync('pages/**/index.js', {
+ cwd: path.join(ROOT_PATH, 'app/assets/javascripts'),
+ });
+ watchAutoEntries = [path.join(ROOT_PATH, 'app/assets/javascripts/pages/')];
function generateAutoEntries(path, prefix = '.') {
const chunkPath = path.replace(/\/index\.js$/, '');
@@ -38,15 +36,16 @@ function generateEntries() {
autoEntries[chunkName] = `${prefix}/${path}`;
}
- pageEntries.forEach(( path ) => generateAutoEntries(path));
+ pageEntries.forEach(path => generateAutoEntries(path));
autoEntriesCount = Object.keys(autoEntries).length;
const manualEntries = {
- common: './commons/index.js',
- main: './main.js',
- raven: './raven/index.js',
- webpack_runtime: './webpack.js',
+ common: './commons/index.js',
+ main: './main.js',
+ raven: './raven/index.js',
+ webpack_runtime: './webpack.js',
+ ide: './ide/index.js',
};
return Object.assign(manualEntries, autoEntries);
@@ -90,8 +89,8 @@ const config = {
{
loader: 'worker-loader',
options: {
- inline: true
- }
+ inline: true,
+ },
},
{ loader: 'babel-loader' },
],
@@ -102,7 +101,7 @@ const config = {
loader: 'file-loader',
options: {
name: '[name].[hash].[ext]',
- }
+ },
},
{
test: /katex.css$/,
@@ -112,8 +111,8 @@ const config = {
{
loader: 'css-loader',
options: {
- name: '[name].[hash].[ext]'
- }
+ name: '[name].[hash].[ext]',
+ },
},
],
},
@@ -123,7 +122,7 @@ const config = {
loader: 'file-loader',
options: {
name: '[name].[hash].[ext]',
- }
+ },
},
{
test: /monaco-editor\/\w+\/vs\/loader\.js$/,
@@ -131,7 +130,7 @@ const config = {
{ loader: 'exports-loader', options: 'l.global' },
{ loader: 'imports-loader', options: 'l=>{},this=>l,AMDLoader=>this,module=>undefined' },
],
- }
+ },
],
noParse: [/monaco-editor\/\w+\/vs\//],
@@ -149,10 +148,10 @@ const config = {
source: false,
chunks: false,
modules: false,
- assets: true
+ assets: true,
});
return JSON.stringify(stats, null, 2);
- }
+ },
}),
// prevent pikaday from including moment.js
@@ -169,7 +168,7 @@ const config = {
new NameAllModulesPlugin(),
// assign deterministic chunk ids
- new webpack.NamedChunksPlugin((chunk) => {
+ new webpack.NamedChunksPlugin(chunk => {
if (chunk.name) {
return chunk.name;
}
@@ -186,9 +185,12 @@ const config = {
const pagesBase = path.join(ROOT_PATH, 'app/assets/javascripts/pages');
if (m.resource.indexOf(pagesBase) === 0) {
- moduleNames.push(path.relative(pagesBase, m.resource)
- .replace(/\/index\.[a-z]+$/, '')
- .replace(/\//g, '__'));
+ moduleNames.push(
+ path
+ .relative(pagesBase, m.resource)
+ .replace(/\/index\.[a-z]+$/, '')
+ .replace(/\//g, '__')
+ );
} else {
moduleNames.push(path.relative(m.context, m.resource));
}
@@ -196,7 +198,8 @@ const config = {
chunk.forEachModule(collectModuleNames);
- const hash = crypto.createHash('sha256')
+ const hash = crypto
+ .createHash('sha256')
.update(moduleNames.join('_'))
.digest('hex');
@@ -208,13 +211,13 @@ const config = {
names: ['main', 'common', 'webpack_runtime'],
}),
- // enable scope hoisting
- new webpack.optimize.ModuleConcatenationPlugin(),
-
// copy pre-compiled vendor libraries verbatim
new CopyWebpackPlugin([
{
- from: path.join(ROOT_PATH, `node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs`),
+ from: path.join(
+ ROOT_PATH,
+ `node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs`
+ ),
to: 'monaco-editor/vs',
transform: function(content, path) {
if (/\.js$/.test(path) && !/worker/i.test(path) && !/typescript/i.test(path)) {
@@ -227,23 +230,23 @@ const config = {
);
}
return content;
- }
- }
+ },
+ },
]),
],
resolve: {
extensions: ['.js'],
alias: {
- '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
- 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'),
- 'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'),
- 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'),
- 'images': path.join(ROOT_PATH, 'app/assets/images'),
- 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
- 'vue$': 'vue/dist/vue.esm.js',
- 'spec': path.join(ROOT_PATH, 'spec/javascripts'),
- }
+ '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
+ emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
+ empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'),
+ icons: path.join(ROOT_PATH, 'app/views/shared/icons'),
+ images: path.join(ROOT_PATH, 'app/assets/images'),
+ vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
+ vue$: 'vue/dist/vue.esm.js',
+ spec: path.join(ROOT_PATH, 'spec/javascripts'),
+ },
},
// sqljs requires fs
@@ -258,13 +261,14 @@ if (IS_PRODUCTION) {
new webpack.NoEmitOnErrorsPlugin(),
new webpack.LoaderOptionsPlugin({
minimize: true,
- debug: false
+ debug: false,
}),
+ new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.optimize.UglifyJsPlugin({
- sourceMap: true
+ sourceMap: true,
}),
new webpack.DefinePlugin({
- 'process.env': { NODE_ENV: JSON.stringify('production') }
+ 'process.env': { NODE_ENV: JSON.stringify('production') },
})
);
@@ -283,7 +287,7 @@ if (IS_DEV_SERVER) {
headers: { 'Access-Control-Allow-Origin': '*' },
stats: 'errors-only',
hot: DEV_SERVER_LIVERELOAD,
- inline: DEV_SERVER_LIVERELOAD
+ inline: DEV_SERVER_LIVERELOAD,
};
config.plugins.push(
// watch node_modules for changes if we encounter a missing module compile error
@@ -299,10 +303,12 @@ if (IS_DEV_SERVER) {
];
// report our auto-generated bundle count
- console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`);
+ console.log(
+ `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`
+ );
callback();
- })
+ });
},
}
);
diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
index bcdae272209..a96ea7d9db4 100644
--- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
+++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
@@ -12,7 +12,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
end
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]['path']
+ Gitlab.config.repositories.storages[repository_storage].legacy_disk_path
end
def repository_path
diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
index 8fb1f9d5e73..bddc234db25 100644
--- a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
+++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
@@ -60,7 +60,7 @@ class RemoveDotGitFromGroupNames < ActiveRecord::Migration
def move_namespace(group_id, path_was, path)
repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
- Gitlab.config.repositories.storages[row['repository_storage']]['path']
+ Gitlab.config.repositories.storages[row['repository_storage']].legacy_disk_path
end.compact
# Move the namespace directory in all storages paths used by member projects
diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
index 61dcc8c54f5..7c28d934c29 100644
--- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
+++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
@@ -71,7 +71,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
route_exists = route_exists?(path)
Gitlab.config.repositories.storages.each_value do |storage|
- if route_exists || path_exists?(path, storage['path'])
+ if route_exists || path_exists?(path, storage.legacy_disk_path)
counter += 1
path = "#{base}#{counter}"
@@ -84,7 +84,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
def move_namespace(namespace_id, path_was, path)
repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
- Gitlab.config.repositories.storages[row['repository_storage']]['path']
+ Gitlab.config.repositories.storages[row['repository_storage']].legacy_disk_path
end.compact
# Move the namespace directory in all storages paths used by member projects
diff --git a/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb b/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb
index af6d10b5158..1199073ed3a 100644
--- a/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb
+++ b/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb
@@ -154,7 +154,7 @@ class ProjectForeignKeysWithCascadingDeletes < ActiveRecord::Migration
end
def add_foreign_key_if_not_exists(source, target, column:)
- return if foreign_key_exists?(source, column)
+ return if foreign_key_exists?(source, target, column: column)
add_concurrent_foreign_key(source, target, column: column)
end
@@ -175,12 +175,6 @@ class ProjectForeignKeysWithCascadingDeletes < ActiveRecord::Migration
rescue ArgumentError
end
- def foreign_key_exists?(table, column)
- foreign_keys(table).any? do |key|
- key.options[:column] == column.to_s
- end
- end
-
def connection
# Rails memoizes connection objects, but this causes them to be shared
# amongst threads; we don't want that.
diff --git a/db/migrate/20170601163708_add_artifacts_store_to_ci_build.rb b/db/migrate/20170601163708_add_artifacts_store_to_ci_build.rb
new file mode 100644
index 00000000000..e82109190a7
--- /dev/null
+++ b/db/migrate/20170601163708_add_artifacts_store_to_ci_build.rb
@@ -0,0 +1,10 @@
+class AddArtifactsStoreToCiBuild < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:ci_builds, :artifacts_file_store, :integer)
+ add_column(:ci_builds, :artifacts_metadata_store, :integer)
+ end
+end
diff --git a/db/migrate/20170703102400_add_stage_id_foreign_key_to_builds.rb b/db/migrate/20170703102400_add_stage_id_foreign_key_to_builds.rb
index 68b947583d3..a89d348b127 100644
--- a/db/migrate/20170703102400_add_stage_id_foreign_key_to_builds.rb
+++ b/db/migrate/20170703102400_add_stage_id_foreign_key_to_builds.rb
@@ -10,13 +10,13 @@ class AddStageIdForeignKeyToBuilds < ActiveRecord::Migration
add_concurrent_index(:ci_builds, :stage_id)
end
- unless foreign_key_exists?(:ci_builds, :stage_id)
+ unless foreign_key_exists?(:ci_builds, :ci_stages, column: :stage_id)
add_concurrent_foreign_key(:ci_builds, :ci_stages, column: :stage_id, on_delete: :cascade)
end
end
def down
- if foreign_key_exists?(:ci_builds, :stage_id)
+ if foreign_key_exists?(:ci_builds, column: :stage_id)
remove_foreign_key(:ci_builds, column: :stage_id)
end
@@ -24,12 +24,4 @@ class AddStageIdForeignKeyToBuilds < ActiveRecord::Migration
remove_concurrent_index(:ci_builds, :stage_id)
end
end
-
- private
-
- def foreign_key_exists?(table, column)
- foreign_keys(:ci_builds).any? do |key|
- key.options[:column] == column.to_s
- end
- end
end
diff --git a/db/migrate/20170713104829_add_foreign_key_to_merge_requests.rb b/db/migrate/20170713104829_add_foreign_key_to_merge_requests.rb
index c25d4fd5986..c409915ceed 100644
--- a/db/migrate/20170713104829_add_foreign_key_to_merge_requests.rb
+++ b/db/migrate/20170713104829_add_foreign_key_to_merge_requests.rb
@@ -23,23 +23,15 @@ class AddForeignKeyToMergeRequests < ActiveRecord::Migration
merge_requests.update_all(head_pipeline_id: nil)
end
- unless foreign_key_exists?(:merge_requests, :head_pipeline_id)
+ unless foreign_key_exists?(:merge_requests, column: :head_pipeline_id)
add_concurrent_foreign_key(:merge_requests, :ci_pipelines,
column: :head_pipeline_id, on_delete: :nullify)
end
end
def down
- if foreign_key_exists?(:merge_requests, :head_pipeline_id)
+ if foreign_key_exists?(:merge_requests, column: :head_pipeline_id)
remove_foreign_key(:merge_requests, column: :head_pipeline_id)
end
end
-
- private
-
- def foreign_key_exists?(table, column)
- foreign_keys(table).any? do |key|
- key.options[:column] == column.to_s
- end
- end
end
diff --git a/db/migrate/20170825015534_add_file_store_to_lfs_objects.rb b/db/migrate/20170825015534_add_file_store_to_lfs_objects.rb
new file mode 100644
index 00000000000..41bb031014f
--- /dev/null
+++ b/db/migrate/20170825015534_add_file_store_to_lfs_objects.rb
@@ -0,0 +1,31 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddFileStoreToLfsObjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column(:lfs_objects, :file_store, :integer)
+ end
+end
diff --git a/db/migrate/20170918072949_add_file_store_job_artifacts.rb b/db/migrate/20170918072949_add_file_store_job_artifacts.rb
new file mode 100644
index 00000000000..b1f1bea6deb
--- /dev/null
+++ b/db/migrate/20170918072949_add_file_store_job_artifacts.rb
@@ -0,0 +1,10 @@
+class AddFileStoreJobArtifacts < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+ DOWNTIME = false
+
+ def change
+ add_column(:ci_job_artifacts, :file_store, :integer)
+ end
+end
diff --git a/db/migrate/20171214144320_add_store_column_to_uploads.rb b/db/migrate/20171214144320_add_store_column_to_uploads.rb
new file mode 100644
index 00000000000..e35798e2c41
--- /dev/null
+++ b/db/migrate/20171214144320_add_store_column_to_uploads.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddStoreColumnToUploads < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:uploads, :store, :integer)
+ end
+end
diff --git a/db/migrate/20180102220145_add_pages_https_only_to_projects.rb b/db/migrate/20180102220145_add_pages_https_only_to_projects.rb
new file mode 100644
index 00000000000..ef6bc6896c0
--- /dev/null
+++ b/db/migrate/20180102220145_add_pages_https_only_to_projects.rb
@@ -0,0 +1,9 @@
+class AddPagesHttpsOnlyToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :projects, :pages_https_only, :boolean
+ end
+end
diff --git a/db/migrate/20180109183319_change_default_value_for_pages_https_only.rb b/db/migrate/20180109183319_change_default_value_for_pages_https_only.rb
new file mode 100644
index 00000000000..c242e1b0d24
--- /dev/null
+++ b/db/migrate/20180109183319_change_default_value_for_pages_https_only.rb
@@ -0,0 +1,13 @@
+class ChangeDefaultValueForPagesHttpsOnly < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_default :projects, :pages_https_only, true
+ end
+
+ def down
+ change_column_default :projects, :pages_https_only, nil
+ end
+end
diff --git a/db/migrate/20180223144945_add_allow_local_requests_from_hooks_and_services_to_application_settings.rb b/db/migrate/20180223144945_add_allow_local_requests_from_hooks_and_services_to_application_settings.rb
new file mode 100644
index 00000000000..c994a54698b
--- /dev/null
+++ b/db/migrate/20180223144945_add_allow_local_requests_from_hooks_and_services_to_application_settings.rb
@@ -0,0 +1,18 @@
+class AddAllowLocalRequestsFromHooksAndServicesToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :allow_local_requests_from_hooks_and_services,
+ :boolean,
+ default: false,
+ allow_null: false)
+ end
+
+ def down
+ remove_column(:application_settings, :allow_local_requests_from_hooks_and_services)
+ end
+end
diff --git a/db/migrate/20180305095250_create_internal_ids_table.rb b/db/migrate/20180305095250_create_internal_ids_table.rb
new file mode 100644
index 00000000000..432086fe98b
--- /dev/null
+++ b/db/migrate/20180305095250_create_internal_ids_table.rb
@@ -0,0 +1,15 @@
+class CreateInternalIdsTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :internal_ids, id: :bigserial do |t|
+ t.references :project, null: false, foreign_key: { on_delete: :cascade }
+ t.integer :usage, null: false
+ t.integer :last_value, null: false
+
+ t.index [:usage, :project_id], unique: true
+ end
+ end
+end
diff --git a/db/migrate/20180320182229_add_indexes_for_user_activity_queries.rb b/db/migrate/20180320182229_add_indexes_for_user_activity_queries.rb
new file mode 100644
index 00000000000..824bbb3ac05
--- /dev/null
+++ b/db/migrate/20180320182229_add_indexes_for_user_activity_queries.rb
@@ -0,0 +1,40 @@
+class AddIndexesForUserActivityQueries < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :events, [:author_id, :project_id] unless index_exists?(:events, [:author_id, :project_id])
+ add_concurrent_index :user_interacted_projects, :user_id unless index_exists?(:user_interacted_projects, :user_id)
+ end
+
+ def down
+ remove_concurrent_index :events, [:author_id, :project_id] if index_exists?(:events, [:author_id, :project_id])
+
+ patch_foreign_keys do
+ remove_concurrent_index :user_interacted_projects, :user_id if index_exists?(:user_interacted_projects, :user_id)
+ end
+ end
+
+ private
+
+ def patch_foreign_keys
+ return yield if Gitlab::Database.postgresql?
+
+ # MySQL doesn't like to remove the index with a foreign key using it.
+ remove_foreign_key :user_interacted_projects, :users if fk_exists?(:user_interacted_projects, :user_id)
+
+ yield
+
+ # Let's re-add the foreign key using the existing index on (user_id, project_id)
+ add_concurrent_foreign_key :user_interacted_projects, :users, column: :user_id unless fk_exists?(:user_interacted_projects, :user_id)
+ end
+
+ def fk_exists?(table, column)
+ foreign_keys(table).any? do |key|
+ key.options[:column] == column.to_s
+ end
+ end
+end
diff --git a/db/migrate/20180323150945_add_push_to_merge_request_to_notification_settings.rb b/db/migrate/20180323150945_add_push_to_merge_request_to_notification_settings.rb
new file mode 100644
index 00000000000..12b8875d8dc
--- /dev/null
+++ b/db/migrate/20180323150945_add_push_to_merge_request_to_notification_settings.rb
@@ -0,0 +1,9 @@
+class AddPushToMergeRequestToNotificationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :notification_settings, :push_to_merge_request, :boolean
+ end
+end
diff --git a/db/migrate/20180327101207_remove_index_from_events_table.rb b/db/migrate/20180327101207_remove_index_from_events_table.rb
new file mode 100644
index 00000000000..172441da65b
--- /dev/null
+++ b/db/migrate/20180327101207_remove_index_from_events_table.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveIndexFromEventsTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index :events, :author_id
+ end
+
+ def down
+ add_concurrent_index :events, :author_id
+ end
+end
diff --git a/db/post_migrate/20180220150310_remove_empty_extern_uid_auth0_identities.rb b/db/post_migrate/20180220150310_remove_empty_extern_uid_auth0_identities.rb
new file mode 100644
index 00000000000..2d5a8617169
--- /dev/null
+++ b/db/post_migrate/20180220150310_remove_empty_extern_uid_auth0_identities.rb
@@ -0,0 +1,25 @@
+class RemoveEmptyExternUidAuth0Identities < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Identity < ActiveRecord::Base
+ self.table_name = 'identities'
+ include EachBatch
+ end
+
+ def up
+ broken_auth0_identities.each_batch do |identity|
+ identity.delete_all
+ end
+ end
+
+ def broken_auth0_identities
+ Identity.where(provider: 'auth0', extern_uid: [nil, ''])
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20180223124427_build_user_interacted_projects_table.rb b/db/post_migrate/20180223124427_build_user_interacted_projects_table.rb
index d1a29a5c71b..9addd36dca6 100644
--- a/db/post_migrate/20180223124427_build_user_interacted_projects_table.rb
+++ b/db/post_migrate/20180223124427_build_user_interacted_projects_table.rb
@@ -26,11 +26,11 @@ class BuildUserInteractedProjectsTable < ActiveRecord::Migration
def down
execute "TRUNCATE user_interacted_projects"
- if foreign_key_exists?(:user_interacted_projects, :user_id)
+ if foreign_key_exists?(:user_interacted_projects, :users)
remove_foreign_key :user_interacted_projects, :users
end
- if foreign_key_exists?(:user_interacted_projects, :project_id)
+ if foreign_key_exists?(:user_interacted_projects, :projects)
remove_foreign_key :user_interacted_projects, :projects
end
@@ -115,7 +115,7 @@ class BuildUserInteractedProjectsTable < ActiveRecord::Migration
end
def create_fk(table, target, column)
- return if foreign_key_exists?(table, column)
+ return if foreign_key_exists?(table, target, column: column)
add_foreign_key table, target, column: column, on_delete: :cascade
end
@@ -158,11 +158,11 @@ class BuildUserInteractedProjectsTable < ActiveRecord::Migration
add_concurrent_index :user_interacted_projects, [:project_id, :user_id], unique: true, name: UNIQUE_INDEX_NAME
end
- unless foreign_key_exists?(:user_interacted_projects, :user_id)
+ unless foreign_key_exists?(:user_interacted_projects, :users, column: :user_id)
add_concurrent_foreign_key :user_interacted_projects, :users, column: :user_id, on_delete: :cascade
end
- unless foreign_key_exists?(:user_interacted_projects, :project_id)
+ unless foreign_key_exists?(:user_interacted_projects, :projects, column: :project_id)
add_concurrent_foreign_key :user_interacted_projects, :projects, column: :project_id, on_delete: :cascade
end
end
diff --git a/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb b/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb
new file mode 100644
index 00000000000..db5165dbe70
--- /dev/null
+++ b/db/post_migrate/20180305100050_remove_permanent_from_redirect_routes.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemovePermanentFromRedirectRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ INDEX_NAME_PERM = "index_redirect_routes_on_path_text_pattern_ops_where_permanent"
+ INDEX_NAME_TEMP = "index_redirect_routes_on_path_text_pattern_ops_where_temporary"
+
+ def up
+ # These indexes were created on Postgres only in:
+ # ReworkRedirectRoutesIndexes:
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/16211
+ if Gitlab::Database.postgresql?
+ disable_statement_timeout
+
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};"
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};"
+ end
+
+ remove_column(:redirect_routes, :permanent)
+ end
+
+ def down
+ add_column(:redirect_routes, :permanent, :boolean)
+
+ if Gitlab::Database.postgresql?
+ disable_statement_timeout
+
+ execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);")
+ execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;")
+ end
+ end
+end
diff --git a/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb b/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb
new file mode 100644
index 00000000000..d6fb4f06695
--- /dev/null
+++ b/db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPathIndexToRedirectRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_redirect_routes_on_path_unique_text_pattern_ops'
+
+ # Indexing on LOWER(path) varchar_pattern_ops speeds up the LIKE query in
+ # RedirectRoute.matching_path_and_descendants
+ #
+ # This same index is also added in the `ReworkRedirectRoutesIndexes` so this
+ # is a no-op in most cases. But this migration is also called from the
+ # `setup_postgresql.rake` task when setting up a new database, in which case
+ # we want to create the index.
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ disable_statement_timeout
+
+ unless index_exists_by_name?(:redirect_routes, INDEX_NAME)
+ execute("CREATE UNIQUE INDEX CONCURRENTLY #{INDEX_NAME} ON redirect_routes (lower(path) varchar_pattern_ops);")
+ end
+ end
+
+ def down
+ # Do nothing in the DOWN. Since the index above is originally created in the
+ # `ReworkRedirectRoutesIndexes`. This migration wouldn't have actually
+ # created any new index.
+ #
+ # This migration is only here to be called form `setup_postgresql.rake` so
+ # any newly created database would have this index.
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ab4370e2754..77b3d836287 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180309160427) do
+ActiveRecord::Schema.define(version: 20180327101207) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -157,6 +157,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
t.boolean "authorized_keys_enabled", default: true, null: false
t.string "auto_devops_domain"
t.boolean "pages_domain_verification_enabled", default: true, null: false
+ t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -306,6 +307,8 @@ ActiveRecord::Schema.define(version: 20180309160427) do
t.integer "auto_canceled_by_id"
t.boolean "retried"
t.integer "stage_id"
+ t.integer "artifacts_file_store"
+ t.integer "artifacts_metadata_store"
t.boolean "protected"
t.integer "failure_reason"
end
@@ -344,6 +347,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
t.integer "project_id", null: false
t.integer "job_id", null: false
t.integer "file_type", null: false
+ t.integer "file_store"
t.integer "size", limit: 8
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -727,7 +731,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
end
add_index "events", ["action"], name: "index_events_on_action", using: :btree
- add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
+ add_index "events", ["author_id", "project_id"], name: "index_events_on_author_id_and_project_id", using: :btree
add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree
add_index "events", ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id", using: :btree
@@ -866,6 +870,14 @@ ActiveRecord::Schema.define(version: 20180309160427) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
+ create_table "internal_ids", id: :bigserial, force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.integer "usage", null: false
+ t.integer "last_value", null: false
+ end
+
+ add_index "internal_ids", ["usage", "project_id"], name: "index_internal_ids_on_usage_and_project_id", unique: true, using: :btree
+
create_table "issue_assignees", id: false, force: :cascade do |t|
t.integer "user_id", null: false
t.integer "issue_id", null: false
@@ -999,6 +1011,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "file"
+ t.integer "file_store"
end
add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
@@ -1286,6 +1299,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
t.boolean "merge_merge_request"
t.boolean "failed_pipeline"
t.boolean "success_pipeline"
+ t.boolean "push_to_merge_request"
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
@@ -1503,6 +1517,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
t.boolean "merge_requests_ff_only_enabled", default: false
t.boolean "merge_requests_rebase_enabled", default: false, null: false
t.integer "jobs_cache_index"
+ t.boolean "pages_https_only", default: true
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1590,7 +1605,6 @@ ActiveRecord::Schema.define(version: 20180309160427) do
t.string "path", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.boolean "permanent"
end
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree
@@ -1812,6 +1826,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
t.datetime "created_at", null: false
t.string "mount_point"
t.string "secret"
+ t.integer "store"
end
add_index "uploads", ["checksum"], name: "index_uploads_on_checksum", using: :btree
@@ -1855,6 +1870,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
end
add_index "user_interacted_projects", ["project_id", "user_id"], name: "index_user_interacted_projects_on_project_id_and_user_id", unique: true, using: :btree
+ add_index "user_interacted_projects", ["user_id"], name: "index_user_interacted_projects_on_user_id", using: :btree
create_table "user_synced_attributes_metadata", force: :cascade do |t|
t.boolean "name_synced", default: false
@@ -2058,6 +2074,7 @@ ActiveRecord::Schema.define(version: 20180309160427) do
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "internal_ids", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 05fa444657c..be805a2ccc4 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -11,35 +11,62 @@ GitLab offers the most scalable Git-based fully integrated platform for
software development, with flexible products and subscriptions.
To understand what features you have access to, check the [GitLab subscriptions](#gitlab-subscriptions) below.
-## Shortcuts to GitLab's most visited docs
+**Shortcuts to GitLab's most visited docs:**
-| [GitLab CI/CD](ci/README.md) | Other |
+| General documentation | GitLab CI/CD docs |
| :----- | :----- |
-| [Quick start guide](ci/quick_start/README.md) | [API](api/README.md) |
-| [Configuring `.gitlab-ci.yml`](ci/yaml/README.md) | [SSH authentication](ssh/README.md) |
-| [Using Docker images](ci/docker/using_docker_images.md) | [GitLab Pages](user/project/pages/index.md) |
+| [User documentation](user/index.md) | [GitLab CI/CD](ci/README.md) |
+| [Administrator documentation](administration/index.md) | [GitLab CI/CD quick start guide](ci/quick_start/README.md) |
+| [Contributor documentation](#contributor-documentation) | [Configuring `.gitlab-ci.yml`](ci/yaml/README.md) |
+| [Getting started with GitLab](#getting-started-with-gitlab) | [Using Docker images](ci/docker/using_docker_images.md) |
+| [API](api/README.md) | [Auto DevOps](topics/autodevops/index.md) |
+| [SSH authentication](ssh/README.md) | [Kubernetes integration](user/project/clusters/index.md)|
+| [GitLab Pages](user/project/pages/index.md) | [GitLab Container Registry](user/project/container_registry.md) |
+
+## Complete DevOps with GitLab
+
+GitLab is the first single application for software development, security,
+and operations that enables Concurrent DevOps, making the software lifecycle
+three times faster and radically improving the speed of business. GitLab
+provides solutions for all the stages of the DevOps lifecycle:
+[plan](#plan), [create](#create), [verify](#verify), [package](#package),
+[release](#release), [configure](#configure), [monitor](#monitor).
+
+![DevOps Lifecycle](img/devops_lifecycle.png)
+
+### Plan
+
+Whether you use Waterfall, Agile, or Conversational Development,
+GitLab streamlines your collaborative workflows. Visualize, prioritize,
+coordinate, and track your progress your way with GitLab’s flexible project
+management tools.
+
+- Chat operations
+ - [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md)
+ - [Slack slash commands](user/project/integrations/slack_slash_commands.md)
+- [Discussions](user/discussions/index.md): Threads, comments, and resolvable discussions in issues, commits, and merge requests.
+- [Issues](user/project/issues/index.md)
+- [Project Issue Board](user/project/issue_board.md)
+- [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests.
+- [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles.
+- [Milestones](user/project/milestones/index.md): Organize issues and merge requests into a cohesive group, optionally setting a due date.
+- [Todos](workflow/todos.md): A chronological list of to-dos that are waiting for your input, all in a simple dashboard.
+- [GitLab Quick Actions](user/project/quick_actions.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
-- [User documentation](user/index.md)
-- [Administrator documentation](administration/index.md)
-- [Contributor documentation](#contributor-documentation)
+#### Migrate and import your projects from other platforms
-## Getting started with GitLab
+- [Importing to GitLab](user/project/import/index.md): Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
+- [Migrating from SVN](workflow/importing/migrating_from_svn.md): Convert a SVN repository to Git and GitLab.
-- [GitLab Basics](gitlab-basics/README.md): Start working on your command line and on GitLab.
-- [GitLab Workflow](workflow/README.md): Enhance your workflow with the best of GitLab Workflow.
- - See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
-- [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown).
-- [GitLab Quick Actions](user/project/quick_actions.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
-- [Auto DevOps](topics/autodevops/index.md)
+### Create
-### User account
+Consolidate source code into a single [DVCS](https://en.wikipedia.org/wiki/Distributed_version_control)
+that’s easily managed and controlled without disrupting your workflow.
+GitLab’s git repositories come complete with branching tools and access
+controls, providing a scalable, single source of truth for collaborating
+on projects and code.
-- [User account](user/profile/index.md): Manage your account
- - [Authentication](topics/authentication/index.md): Account security with two-factor authentication, setup your ssh keys and deploy keys for secure access to your projects.
- - [Profile settings](user/profile/index.md#profile-settings): Manage your profile settings, two factor authentication and more.
-- [User permissions](user/permissions.md): Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
-
-### Projects and groups
+#### Projects and groups
- [Projects](user/project/index.md):
- [Project settings](user/project/settings/index.md)
@@ -54,7 +81,7 @@ To understand what features you have access to, check the [GitLab subscriptions]
- [Snippets](user/snippets.md): Snippets allow you to create little bits of code.
- [Wikis](user/project/wiki/index.md): Enhance your repository documentation with built-in wikis.
-### Repository
+#### Repositories
Manage your [repositories](user/project/repository/index.md) from the UI (user interface):
@@ -72,51 +99,88 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
- [Commits](user/project/repository/index.md#commits)
- [Signing commits](user/project/repository/gpg_signed_commits/index.md): use GPG to sign your commits.
-### Issues and Merge Requests (MRs)
+#### Integrations
+
+- [Project Services](user/project/integrations/project_services.md): Integrate a project with external services, such as CI and chat.
+- [GitLab Integration](integration/README.md): Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication.
+- [Trello Power-Up](integration/trello_power_up.md): Integrate with GitLab's Trello Power-Up
+
+#### Automation
+
+- [API](api/README.md): Automate GitLab via a simple and powerful API.
+- [GitLab Webhooks](user/project/integrations/webhooks.md): Let GitLab notify you when new code has been pushed to your project.
+
+### Verify
+
+Spot errors sooner and shorten feedback cycles with built-in code review, code testing,
+Code Quality, and Review Apps. Customize your approval workflow controls, automatically
+test the quality of your code, and spin up a staging environment for every code change.
+GitLab Continuous Integration is the most popular next generation testing system that
+auto scales to run your tests faster.
-- [Discussions](user/discussions/index.md): Threads, comments, and resolvable discussions in issues, commits, and merge requests.
-- [Issues](user/project/issues/index.md)
-- [Project issue Board](user/project/issue_board.md)
-- [Group Issue Board](user/project/issue_board.md#group-issue-board)
-- [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests.
-- [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles.
- [Merge Requests](user/project/merge_requests/index.md)
- [Work In Progress Merge Requests](user/project/merge_requests/work_in_progress_merge_requests.md)
- [Merge Request discussion resolution](user/discussions/index.md#moving-a-single-discussion-to-a-new-issue): Resolve discussions, move discussions in a merge request to an issue, only allow merge requests to be merged if all discussions are resolved.
- [Checkout merge requests locally](user/project/merge_requests/index.md#checkout-merge-requests-locally)
- [Cherry-pick](user/project/merge_requests/cherry_pick_changes.md)
-- [Milestones](user/project/milestones/index.md): Organize issues and merge requests into a cohesive group, optionally setting a due date.
-- [Todos](workflow/todos.md): A chronological list of to-dos that are waiting for your input, all in a simple dashboard.
+- [Review Apps](ci/review_apps/index.md): Preview changes to your app right from a merge request.
-### Git and GitLab
+### Package
-- [Git](topics/git/index.md): Getting started with Git, branching strategies, Git LFS, advanced use.
-- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf): Download a PDF describing the most used Git operations.
-- [GitLab Flow](workflow/gitlab_flow.md): explore the best of Git with the GitLab Flow strategy.
+GitLab Container Registry gives you the enhanced security and access controls of
+custom Docker images without 3rd party add-ons. Easily upload and download images
+from GitLab CI/CD with full Git repository management integration.
-### Migrate and import your projects from other platforms
+- [GitLab CI/CD](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab.
+- [GitLab Container Registry](user/project/container_registry.md): Learn how to use GitLab's built-in Container Registry.
-- [Importing to GitLab](user/project/import/index.md): Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
-- [Migrating from SVN](workflow/importing/migrating_from_svn.md): Convert a SVN repository to Git and GitLab.
+### Release
+
+Spend less time configuring your tools, and more time creating. Whether you’re
+deploying to one server or thousands, build, test, and release your code
+confidently and securely with GitLab’s built-in Continuous Delivery and Deployment.
+
+- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy a static site directly from GitLab.
+- [Auto Deploy](topics/autodevops/index.md#auto-deploy): Configure GitLab CI for the deployment of your application.
+- [Environments and deployments](ci/environments.md): With environments, you can control the continuous deployment of your software within GitLab.
-### Continuous Integration, Delivery, and Deployment
+### Configure
+
+Automate your entire workflow from build to deploy and monitoring with GitLab
+Auto Devops. Best practice templates get you started with minimal to zero
+configuration. Then customize everything from buildpacks to CI/CD.
+
+- [Auto DevOps](topics/autodevops/index.md)
+
+### Monitor
+
+Measure how long it takes to go from planning to monitoring and ensure your
+applications are always responsive and available. GitLab collects and displays
+performance metrics for deployed apps using Prometheus so you can know in an
+instant how code changes impact your production environment.
-- [GitLab CI](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab.
- - [Auto Deploy](ci/autodeploy/index.md): Configure GitLab CI for the deployment of your application.
- - [Review Apps](ci/review_apps/index.md): Preview changes to your app right from a merge request.
- [GitLab Cycle Analytics](user/project/cycle_analytics.md): Cycle Analytics measures the time it takes to go from an [idea to production](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab) for each project you have.
-- [GitLab Container Registry](user/project/container_registry.md): Learn how to use GitLab's built-in Container Registry.
+- [GitLab Performance Monitoring](administration/monitoring/performance/index.md)
-### Automation
+## Getting started with GitLab
-- [API](api/README.md): Automate GitLab via a simple and powerful API.
-- [GitLab Webhooks](user/project/integrations/webhooks.md): Let GitLab notify you when new code has been pushed to your project.
+- [GitLab Basics](gitlab-basics/README.md): Start working on your command line and on GitLab.
+- [GitLab Workflow](workflow/README.md): Enhance your workflow with the best of GitLab Workflow.
+ - See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
+- [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown).
-### Integrations
+### User account
-- [Project Services](user/project/integrations/project_services.md): Integrate a project with external services, such as CI and chat.
-- [GitLab Integration](integration/README.md): Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication.
-- [Trello Power-Up](integration/trello_power_up.md): Integrate with GitLab's Trello Power-Up
+- [User account](user/profile/index.md): Manage your account
+ - [Authentication](topics/authentication/index.md): Account security with two-factor authentication, setup your ssh keys and deploy keys for secure access to your projects.
+ - [Profile settings](user/profile/index.md#profile-settings): Manage your profile settings, two factor authentication and more.
+- [User permissions](user/permissions.md): Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
+
+### Git and GitLab
+
+- [Git](topics/git/index.md): Getting started with Git, branching strategies, Git LFS, advanced use.
+- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf): Download a PDF describing the most used Git operations.
+- [GitLab Flow](workflow/gitlab_flow.md): explore the best of Git with the GitLab Flow strategy.
## Administrator documentation
diff --git a/doc/administration/auth/jwt.md b/doc/administration/auth/jwt.md
new file mode 100644
index 00000000000..b51e705ab52
--- /dev/null
+++ b/doc/administration/auth/jwt.md
@@ -0,0 +1,72 @@
+# JWT OmniAuth provider
+
+To enable the JWT OmniAuth provider, you must register your application with JWT.
+JWT will provide you with a secret key for you to use.
+
+1. On your GitLab server, open the configuration file.
+
+ For Omnibus GitLab:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings.
+1. Add the provider configuration.
+
+ For Omnibus GitLab:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ { name: 'jwt',
+ app_secret: 'YOUR_APP_SECRET',
+ args: {
+ algorithm: 'HS256',
+ uid_claim: 'email',
+ required_claims: ["name", "email"],
+ info_maps: { name: "name", email: "email" },
+ auth_url: 'https://example.com/',
+ valid_within: nil,
+ }
+ }
+ ]
+ ```
+
+ For installation from source:
+
+ ```
+ - { name: 'jwt',
+ app_secret: 'YOUR_APP_SECRET',
+ args: {
+ algorithm: 'HS256',
+ uid_claim: 'email',
+ required_claims: ["name", "email"],
+ info_map: { name: "name", email: "email" },
+ auth_url: 'https://example.com/',
+ valid_within: nil,
+ }
+ }
+ ```
+
+ NOTE: **Note:** For more information on each configuration option refer to
+ the [OmniAuth JWT usage documentation](https://github.com/mbleigh/omniauth-jwt#usage).
+
+1. Change `YOUR_APP_SECRET` to the client secret and set `auth_url` to your redirect URL.
+1. Save the configuration file.
+1. [Reconfigure GitLab][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
+
+On the sign in page there should now be a JWT icon below the regular sign in form.
+Click the icon to begin the authentication process. JWT will ask the user to
+sign in and authorize the GitLab application. If everything goes well, the user
+will be redirected to GitLab and will be signed in.
+
+[reconfigure GitLab]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 69efaf75140..4366590578a 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -111,6 +111,7 @@ server with IMAP authentication on Ubuntu, to be used with Reply by email.
- [Enable/disable GitLab CI/CD](../ci/enable_or_disable_ci.md#site-wide-admin-setting): Enable or disable GitLab CI/CD for your instance.
- [GitLab CI/CD admin settings](../user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time.
- [Job artifacts](job_artifacts.md): Enable, disable, and configure job artifacts (a set of files and directories which are outputted by a job when it completes successfully).
+- [Job traces](job_traces.md): Information about the job traces (logs).
- [Artifacts size and expiration](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size): Define maximum artifacts limits and expiration date.
- [Register Shared and specific Runners](../ci/runners/README.md#registering-a-shared-runner): Learn how to register and configure Shared and specific Runners to your own instance.
- [Shared Runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota): Limit the usage of pipeline minutes for Shared Runners.
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index d86a54daadd..ac3a12930c3 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -87,10 +87,124 @@ _The artifacts are stored by default in
### Using object storage
+>**Notes:**
+- [Introduced][ee-1762] in [GitLab Premium][eep] 9.4.
+- Since version 9.5, artifacts are [browsable], when object storage is enabled.
+ 9.4 lacks this feature.
> Available in [GitLab Premium](https://about.gitlab.com/products/) and
[GitLab.com Silver](https://about.gitlab.com/gitlab-com/).
+> Since version 10.6, available in [GitLab CE](https://about.gitlab.com/products/)
+
+If you don't want to use the local disk where GitLab is installed to store the
+artifacts, you can use an object storage like AWS S3 instead.
+This configuration relies on valid AWS credentials to be configured already.
+Use an [Object storage option][os] like AWS S3 to store job artifacts.
+
+### Object Storage Settings
+
+For source installations the following settings are nested under `artifacts:` and then `object_store:`. On omnibus installs they are prefixed by `artifacts_object_store_`.
+
+| Setting | Description | Default |
+|---------|-------------|---------|
+| `enabled` | Enable/disable object storage | `false` |
+| `remote_directory` | The bucket name where Artfacts will be stored| |
+| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
+| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
+| `connection` | Various connection options described below | |
+
+#### S3 compatible connection settings
+
+The connection settings match those provided by [Fog](https://github.com/fog), and are as follows:
+
+| Setting | Description | Default |
+|---------|-------------|---------|
+| `provider` | Always `AWS` for compatible hosts | AWS |
+| `aws_access_key_id` | AWS credentials, or compatible | |
+| `aws_secret_access_key` | AWS credentials, or compatible | |
+| `region` | AWS region | us-east-1 |
+| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
+| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
+| `path_style` | Set to true to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as false for AWS S3 | false |
+
+**In Omnibus installations:**
+
+_The artifacts are stored by default in
+`/var/opt/gitlab/gitlab-rails/shared/artifacts`._
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
+ the values you want:
+
+ ```ruby
+ gitlab_rails['artifacts_enabled'] = true
+ gitlab_rails['artifacts_object_store_enabled'] = true
+ gitlab_rails['artifacts_object_store_remote_directory'] = "artifacts"
+ gitlab_rails['artifacts_object_store_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-central-1',
+ 'aws_access_key_id' => 'AWS_ACCESS_KEY_ID',
+ 'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY'
+ }
+ ```
+
+ NOTE: For GitLab 9.4+, if you are using AWS IAM profiles, be sure to omit the
+ AWS access key and secret acces key/value pairs. For example:
+
+ ```ruby
+ gitlab_rails['artifacts_object_store_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-central-1',
+ 'use_iam_profile' => true
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Migrate any existing local artifacts to the object storage:
+
+ ```bash
+ gitlab-rake gitlab:artifacts:migrate
+ ```
+
+ Currently this has to be executed manually and it will allow you to
+ migrate the existing artifacts to the object storage, but all new
+ artifacts will still be stored on the local disk. In the future
+ you will be given an option to define a default storage artifacts for all
+ new files.
+
+---
+
+**In installations from source:**
+
+_The artifacts are stored by default in
+`/home/git/gitlab/shared/artifacts`._
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
+ lines:
+
+ ```yaml
+ artifacts:
+ enabled: true
+ object_store:
+ enabled: true
+ remote_directory: "artifacts" # The bucket name
+ connection:
+ provider: AWS # Only AWS supported at the moment
+ aws_access_key_id: AWS_ACESS_KEY_ID
+ aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ region: eu-central-1
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Migrate any existing local artifacts to the object storage:
+
+ ```bash
+ sudo -u git -H bundle exec rake gitlab:artifacts:migrate RAILS_ENV=production
+ ```
-Use an [Object storage option][ee-os] like AWS S3 to store job artifacts.
+ Currently this has to be executed manually and it will allow you to
+ migrate the existing artifacts to the object storage, but all new
+ artifacts will still be stored on the local disk. In the future
+ you will be given an option to define a default storage artifacts for all
+ new files.
## Expiring artifacts
@@ -194,7 +308,7 @@ When clicking on a specific file, [GitLab Workhorse] extracts it
from the archive and the download begins. This implementation saves space,
memory and disk I/O.
-[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
-[restart gitlab]: restart_gitlab.md "How to restart GitLab"
+[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
+[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
-[ee-os]: https://docs.gitlab.com/ee/administration/job_artifacts.html#using-object-storage
+[os]: https://docs.gitlab.com/administration/job_artifacts.html#using-object-storage
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
new file mode 100644
index 00000000000..84a1ffeec98
--- /dev/null
+++ b/doc/administration/job_traces.md
@@ -0,0 +1,42 @@
+# Job traces (logs)
+
+By default, all job traces (logs) are saved to `/var/opt/gitlab/gitlab-ci/builds`
+and `/home/git/gitlab/builds` for Omnibus packages and installations from source
+respectively. The job logs are organized by year and month (for example, `2017_03`),
+and then by project ID.
+
+There isn't a way to automatically expire old job logs, but it's safe to remove
+them if they're taking up too much space. If you remove the logs manually, the
+job output in the UI will be empty.
+
+## Changing the job traces location
+
+To change the location where the job logs will be stored, follow the steps below.
+
+**In Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add or amend the following line:
+
+ ```
+ gitlab_ci['builds_directory'] = '/mnt/to/gitlab-ci/builds'
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+ ```yaml
+ gitlab_ci:
+ # The location where build traces are stored (default: builds/).
+ # Relative paths are relative to Rails.root.
+ builds_path: path/to/builds/
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
+[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md
index ec1cbce1bad..dc4f685d843 100644
--- a/doc/administration/monitoring/performance/performance_bar.md
+++ b/doc/administration/monitoring/performance/performance_bar.md
@@ -13,12 +13,16 @@ It allows you to see (from left to right):
![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png)
- time taken and number of [Gitaly] calls, click through for details of these calls
![Gitaly profiling using the Performance Bar](img/performance_bar_gitaly_calls.png)
-- profile of the code used to generate the page, line by line for either _all_, _app & lib_ , or _views_. In the profile view, the numbers in the left panel represent wall time, cpu time, and number of calls (based on [rblineprof](https://github.com/tmm1/rblineprof)).
+- profile of the code used to generate the page, line by line. In the profile view, the numbers in the left panel represent wall time, cpu time, and number of calls (based on [rblineprof](https://github.com/tmm1/rblineprof)).
![Line profiling using the Performance Bar](img/performance_bar_line_profiling.png)
- time taken and number of calls to Redis
- time taken and number of background jobs created by Sidekiq
- time taken and number of Ruby GC calls
+On the far right is a request selector that allows you to view the same metrics
+(excluding the page timing and line profiler) for any requests made while the
+page was open. Only the first two requests per unique URL are captured.
+
## Enable the Performance Bar via the Admin panel
GitLab Performance Bar is disabled by default. To enable it for a given group,
diff --git a/doc/administration/raketasks/uploads/migrate.md b/doc/administration/raketasks/uploads/migrate.md
new file mode 100644
index 00000000000..0cd33ffc122
--- /dev/null
+++ b/doc/administration/raketasks/uploads/migrate.md
@@ -0,0 +1,74 @@
+# Uploads Migrate Rake Task
+
+## Migrate to Object Storage
+
+After [configuring the object storage](../../uploads.md#using-object-storage) for GitLab's uploads, you may use this task to migrate existing uploads from the local storage to the remote storage.
+
+>**Note:**
+All of the processing will be done in a background worker and requires **no downtime**.
+
+This tasks uses 3 parameters to find uploads to migrate.
+
+>**Note:**
+These parameters are mainly internal to GitLab's structure, you may want to refer to the task list instead below.
+
+Parameter | Type | Description
+--------- | ---- | -----------
+`uploader_class` | string | Type of the uploader to migrate from
+`model_class` | string | Type of the model to migrate from
+`mount_point` | string/symbol | Name of the model's column on which the uploader is mounted on.
+
+This task also accepts some environment variables which you can use to override
+certain values:
+
+Variable | Type | Description
+-------- | ---- | -----------
+`BATCH` | integer | Specifies the size of the batch. Defaults to 200.
+
+** Omnibus Installation**
+
+```bash
+# gitlab-rake gitlab:uploads:migrate[uploader_class, model_class, mount_point]
+
+# Avatars
+gitlab-rake "gitlab:uploads:migrate[AvatarUploader, Project, :avatar]"
+gitlab-rake "gitlab:uploads:migrate[AvatarUploader, Group, :avatar]"
+gitlab-rake "gitlab:uploads:migrate[AvatarUploader, User, :avatar]"
+
+# Attachments
+gitlab-rake "gitlab:uploads:migrate[AttachmentUploader, Note, :attachment]"
+gitlab-rake "gitlab:uploads:migrate[AttachmentUploader, Appearance, :logo]"
+gitlab-rake "gitlab:uploads:migrate[AttachmentUploader, Appearance, :header_logo]"
+
+# Markdown
+gitlab-rake "gitlab:uploads:migrate[FileUploader, Project]"
+gitlab-rake "gitlab:uploads:migrate[PersonalFileUploader, Snippet]"
+gitlab-rake "gitlab:uploads:migrate[NamespaceFileUploader, Snippet]"
+gitlab-rake "gitlab:uploads:migrate[FileUploader, MergeRequest]"
+```
+
+**Source Installation**
+
+>**Note:**
+Use `RAILS_ENV=production` for every task.
+
+```bash
+# sudo -u git -H bundle exec rake gitlab:uploads:migrate
+
+# Avatars
+sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AvatarUploader, Project, :avatar]"
+sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AvatarUploader, Group, :avatar]"
+sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AvatarUploader, User, :avatar]"
+
+# Attachments
+sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AttachmentUploader, Note, :attachment]"
+sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AttachmentUploader, Appearance, :logo]"
+sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AttachmentUploader, Appearance, :header_logo]"
+
+# Markdown
+sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, Project]"
+sudo -u git -H bundle exec rake "gitlab:uploads:migrate[PersonalFileUploader, Snippet]"
+sudo -u git -H bundle exec rake "gitlab:uploads:migrate[NamespaceFileUploader, Snippet]"
+sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, MergeRequest]"
+
+```
diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md
new file mode 100644
index 00000000000..a82735cc72c
--- /dev/null
+++ b/doc/administration/uploads.md
@@ -0,0 +1,209 @@
+# Uploads administration
+
+>**Notes:**
+Uploads represent all user data that may be sent to GitLab as a single file. As an example, avatars and notes' attachments are uploads. Uploads are integral to GitLab functionality, and therefore cannot be disabled.
+
+### Using local storage
+
+>**Notes:**
+This is the default configuration
+
+To change the location where the uploads are stored locally, follow the steps
+below.
+
+---
+
+**In Omnibus installations:**
+
+>**Notes:**
+For historical reasons, uploads are stored into a base directory, which by default is `uploads/-/system`. It is strongly discouraged to change this configuration option on an existing GitLab installation.
+
+_The uploads are stored by default in `/var/opt/gitlab/gitlab-rails/public/uploads/-/system`._
+
+1. To change the storage path for example to `/mnt/storage/uploads`, edit
+ `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['uploads_storage_path'] = "/mnt/storage/"
+ gitlab_rails['uploads_base_dir'] = "uploads"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+_The uploads are stored by default in
+`/home/git/gitlab/public/uploads/-/system`._
+
+1. To change the storage path for example to `/mnt/storage/uploads`, edit
+ `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+ ```yaml
+ uploads:
+ storage_path: /mnt/storage
+ base_dir: uploads
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+### Using object storage
+
+>**Notes:**
+- [Introduced][ee-3867] in [GitLab Enterprise Edition Premium][eep] 10.5.
+
+If you don't want to use the local disk where GitLab is installed to store the
+uploads, you can use an object storage provider like AWS S3 instead.
+This configuration relies on valid AWS credentials to be configured already.
+
+### Object Storage Settings
+
+For source installations the following settings are nested under `uploads:` and then `object_store:`. On omnibus installs they are prefixed by `uploads_object_store_`.
+
+| Setting | Description | Default |
+|---------|-------------|---------|
+| `enabled` | Enable/disable object storage | `false` |
+| `remote_directory` | The bucket name where Uploads will be stored| |
+| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
+| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
+| `connection` | Various connection options described below | |
+
+#### S3 compatible connection settings
+
+The connection settings match those provided by [Fog](https://github.com/fog), and are as follows:
+
+| Setting | Description | Default |
+|---------|-------------|---------|
+| `provider` | Always `AWS` for compatible hosts | AWS |
+| `aws_access_key_id` | AWS credentials, or compatible | |
+| `aws_secret_access_key` | AWS credentials, or compatible | |
+| `region` | AWS region | us-east-1 |
+| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
+| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
+| `path_style` | Set to true to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as false for AWS S3 | false |
+
+**In Omnibus installations:**
+
+_The uploads are stored by default in
+`/var/opt/gitlab/gitlab-rails/public/uploads/-/system`._
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
+ the values you want:
+
+ ```ruby
+ gitlab_rails['uploads_object_store_enabled'] = true
+ gitlab_rails['uploads_object_store_remote_directory'] = "uploads"
+ gitlab_rails['uploads_object_store_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-central-1',
+ 'aws_access_key_id' => 'AWS_ACCESS_KEY_ID',
+ 'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY'
+ }
+ ```
+
+>**Note:**
+If you are using AWS IAM profiles, be sure to omit the AWS access key and secret acces key/value pairs.
+
+ ```ruby
+ gitlab_rails['uploads_object_store_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-central-1',
+ 'use_iam_profile' => true
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Migrate any existing local uploads to the object storage:
+
+>**Notes:**
+These task complies with the `BATCH` environment variable to process uploads in batch (200 by default). All of the processing will be done in a background worker and requires **no downtime**.
+
+ ```bash
+ # gitlab-rake gitlab:uploads:migrate[uploader_class, model_class, mount_point]
+
+ # Avatars
+ gitlab-rake "gitlab:uploads:migrate[AvatarUploader, Project, :avatar]"
+ gitlab-rake "gitlab:uploads:migrate[AvatarUploader, Group, :avatar]"
+ gitlab-rake "gitlab:uploads:migrate[AvatarUploader, User, :avatar]"
+
+ # Attachments
+ gitlab-rake "gitlab:uploads:migrate[AttachmentUploader, Note, :attachment]"
+ gitlab-rake "gitlab:uploads:migrate[AttachmentUploader, Appearance, :logo]"
+ gitlab-rake "gitlab:uploads:migrate[AttachmentUploader, Appearance, :header_logo]"
+
+ # Markdown
+ gitlab-rake "gitlab:uploads:migrate[FileUploader, Project]"
+ gitlab-rake "gitlab:uploads:migrate[PersonalFileUploader, Snippet]"
+ gitlab-rake "gitlab:uploads:migrate[NamespaceFileUploader, Snippet]"
+ gitlab-rake "gitlab:uploads:migrate[FileUploader, MergeRequest]"
+ ```
+
+ Currently this has to be executed manually and it will allow you to
+ migrate the existing uploads to the object storage, but all new
+ uploads will still be stored on the local disk. In the future
+ you will be given an option to define a default storage for all
+ new files.
+
+---
+
+**In installations from source:**
+
+_The uploads are stored by default in
+`/home/git/gitlab/public/uploads/-/system`._
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
+ lines:
+
+ ```yaml
+ uploads:
+ object_store:
+ enabled: true
+ remote_directory: "uploads" # The bucket name
+ connection:
+ provider: AWS # Only AWS supported at the moment
+ aws_access_key_id: AWS_ACESS_KEY_ID
+ aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ region: eu-central-1
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Migrate any existing local uploads to the object storage:
+
+>**Notes:**
+
+- These task comply with the `BATCH` environment variable to process uploads in batch (200 by default). All of the processing will be done in a background worker and requires **no downtime**.
+
+- To migrate in production use `RAILS_ENV=production` environment variable.
+
+ ```bash
+ # sudo -u git -H bundle exec rake gitlab:uploads:migrate
+
+ # Avatars
+ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AvatarUploader, Project, :avatar]"
+ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AvatarUploader, Group, :avatar]"
+ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AvatarUploader, User, :avatar]"
+
+ # Attachments
+ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AttachmentUploader, Note, :attachment]"
+ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AttachmentUploader, Appearance, :logo]"
+ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[AttachmentUploader, Appearance, :header_logo]"
+
+ # Markdown
+ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, Project]"
+ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[PersonalFileUploader, Snippet]"
+ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[NamespaceFileUploader, Snippet]"
+ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, MergeRequest]"
+
+ ```
+
+ Currently this has to be executed manually and it will allow you to
+ migrate the existing uploads to the object storage, but all new
+ uploads will still be stored on the local disk. In the future
+ you will be given an option to define a default storage for all
+ new files.
+
+[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
+[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
+[eep]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition Premium"
+[ee-3867]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3867
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 55c673fd06a..a6b96ba539f 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -536,6 +536,74 @@ Example response:
}
```
+## List Merge Requests associated with a commit
+
+Get a list of Merge Requests related to the specified commit.
+
+```
+GET /projects/:id/repository/commits/:sha/merge_requests
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+| `sha` | string | yes | The commit SHA
+
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/af5b13261899fb2c0db30abdd0af8b07cb44fdc5/merge_requests"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id":45,
+ "iid":1,
+ "project_id":35,
+ "title":"Add new file",
+ "description":"",
+ "state":"opened",
+ "created_at":"2018-03-26T17:26:30.916Z",
+ "updated_at":"2018-03-26T17:26:30.916Z",
+ "target_branch":"master",
+ "source_branch":"test-branch",
+ "upvotes":0,
+ "downvotes":0,
+ "author" : {
+ "web_url" : "https://gitlab.example.com/thedude",
+ "name" : "Jeff Lebowski",
+ "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
+ "username" : "thedude",
+ "state" : "active",
+ "id" : 28
+ },
+ "assignee":null,
+ "source_project_id":35,
+ "target_project_id":35,
+ "labels":[ ],
+ "work_in_progress":false,
+ "milestone":null,
+ "merge_when_pipeline_succeeds":false,
+ "merge_status":"can_be_merged",
+ "sha":"af5b13261899fb2c0db30abdd0af8b07cb44fdc5",
+ "merge_commit_sha":null,
+ "user_notes_count":0,
+ "discussion_locked":null,
+ "should_remove_source_branch":null,
+ "force_remove_source_branch":false,
+ "web_url":"http://https://gitlab.example.com/root/test-project/merge_requests/1",
+ "time_stats":{
+ "time_estimate":0,
+ "total_time_spent":0,
+ "human_time_estimate":null,
+ "human_total_time_spent":null
+ }
+ }
+]
+```
+
[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
[ce-15026]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15026
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index 3a2c398e355..f05ae647577 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -24,6 +24,7 @@ reopen_issue
close_issue
reassign_issue
new_merge_request
+push_to_merge_request
reopen_merge_request
close_merge_request
reassign_merge_request
@@ -75,6 +76,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification |
+| `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification |
| `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
@@ -141,6 +143,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification |
+| `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification |
| `close_merge_request` | boolean | no | Enable/disable this notification |
| `reassign_merge_request` | boolean | no | Enable/disable this notification |
@@ -164,6 +167,7 @@ Example responses:
"close_issue": false,
"reassign_issue": false,
"new_merge_request": false,
+ "push_to_merge_request": false,
"reopen_merge_request": false,
"close_merge_request": false,
"reassign_merge_request": false,
diff --git a/doc/api/search.md b/doc/api/search.md
index d441b556186..107ddaffa6a 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -289,7 +289,7 @@ Search within the specified group.
If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code.
```
-GET /groups/:id/-/search
+GET /groups/:id/search
```
| Attribute | Type | Required | Description |
@@ -305,7 +305,7 @@ The response depends on the requested scope.
### Scope: projects
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=projects&search=flight
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=projects&search=flight
```
Example response:
@@ -336,7 +336,7 @@ Example response:
### Scope: issues
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=issues&search=file
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=issues&search=file
```
Example response:
@@ -401,7 +401,7 @@ Example response:
### Scope: merge_requests
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=merge_requests&search=file
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=merge_requests&search=file
```
Example response:
@@ -478,7 +478,7 @@ Example response:
### Scope: milestones
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/-/search?scope=milestones&search=release
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/3/search?scope=milestones&search=release
```
Example response:
@@ -507,7 +507,7 @@ Search within the specified project.
If a user is not a member of a project and the project is private, a `GET` request on that project will result to a `404` status code.
```
-GET /projects/:id/-/search
+GET /projects/:id/search
```
| Attribute | Type | Required | Description |
@@ -524,7 +524,7 @@ The response depends on the requested scope.
### Scope: issues
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/-/search?scope=issues&search=file
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/search?scope=issues&search=file
```
Example response:
@@ -589,7 +589,7 @@ Example response:
### Scope: merge_requests
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=merge_requests&search=file
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=merge_requests&search=file
```
Example response:
@@ -666,7 +666,7 @@ Example response:
### Scope: milestones
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/-/search?scope=milestones&search=release
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/12/search?scope=milestones&search=release
```
Example response:
@@ -691,7 +691,7 @@ Example response:
### Scope: notes
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=notes&search=maxime
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=notes&search=maxime
```
Example response:
@@ -723,7 +723,7 @@ Example response:
### Scope: wiki_blobs
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=wiki_blobs&search=bye
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=wiki_blobs&search=bye
```
Example response:
@@ -746,7 +746,7 @@ Example response:
### Scope: commits
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=commits&search=bye
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=commits&search=bye
```
Example response:
@@ -777,7 +777,7 @@ Example response:
### Scope: blobs
```bash
-curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/-/search?scope=blobs&search=installation
+curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation
```
Example response:
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 532ae52a184..6aa0e5885db 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -65,7 +65,8 @@ learn how to leverage its potential even more.
environments and use them for different purposes like testing, building and
deploying
- [Job artifacts](../user/project/pipelines/job_artifacts.md)
-- [Git submodules](git_submodules.md): How to run your CI jobs when Git
+- [Caching dependencies](caching/index.md)
+- [Git submodules](git_submodules.md) - How to run your CI jobs when Git
submodules are involved
- [Use SSH keys in your build environment](ssh_keys/README.md)
- [Trigger pipelines through the GitLab API](triggers/README.md)
diff --git a/doc/ci/caching/img/clear_runners_cache.png b/doc/ci/caching/img/clear_runners_cache.png
new file mode 100644
index 00000000000..e5db4a47b3e
--- /dev/null
+++ b/doc/ci/caching/img/clear_runners_cache.png
Binary files differ
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
new file mode 100644
index 00000000000..c159198d16b
--- /dev/null
+++ b/doc/ci/caching/index.md
@@ -0,0 +1,518 @@
+# Cache dependencies in GitLab CI/CD
+
+GitLab CI/CD provides a caching mechanism that can be used to save time
+when your jobs are running.
+
+Caching is about speeding the time a job is executed by reusing the same
+content of a previous job. It can be particularly useful when your are
+developing software that depends on other libraries which are fetched via the
+internet during build time.
+
+If caching is enabled, it's shared between pipelines and jobs by default,
+starting from GitLab 9.0.
+
+Make sure you read the [`cache` reference](../yaml/README.md#cache) to learn
+how it is defined in `.gitlab-ci.yml`.
+
+## Good caching practices
+
+We have the cache from the perspective of the developers (who consume a cache
+within the job) and the cache from the perspective of the Runner. Depending on
+which type of Runner you are using, cache can act differently.
+
+From the perspective of the developer, to ensure maximum availability of the
+cache, when declaring `cache` in your jobs, use one or a mix of the following:
+
+- [Tag your Runners](../runners/README.md#using-tags) and use the tag on jobs
+ that share their cache.
+- [Use sticky Runners](../runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
+ that will be only available to a particular project.
+- [Use a `key`](../yaml/README.md#cache-key) that fits your workflow (e.g.,
+ different caches on each branch). For that, you can take advantage of the
+ [CI/CD predefined variables](../variables/README.md#predefined-variables-environment-variables).
+
+TIP: **Tip:**
+Using the same Runner for your pipeline, is the most simple and efficient way to
+cache files in one stage or pipeline, and pass this cache to subsequent stages
+or pipelines in a guaranteed manner.
+
+From the perspective of the Runner, in order for cache to work effectively, one
+of the following must be true:
+
+- Use a single Runner for all your jobs
+- Use multiple Runners (in autoscale mode or not) that use
+ [distributed caching](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching),
+ where the cache is stored in S3 buckets (like shared Runners on GitLab.com)
+- Use multiple Runners (not in autoscale mode) of the same architecture that
+ share a common network-mounted directory (using NFS or something similar)
+ where the cache will be stored
+
+TIP: **Tip:**
+Read about the [availability of the cache](#availability-of-the-cache)
+to learn more about the internals and get a better idea how cache works.
+
+### Sharing caches across the same branch
+
+Define a cache with the `key: ${CI_COMMIT_REF_SLUG}` so that jobs of each
+branch always use the same cache:
+
+```yaml
+cache:
+ key: ${CI_COMMIT_REF_SLUG}
+```
+
+While this feels like it might be safe from accidentally overwriting the cache,
+it means merge requests get slow first pipelines, which might be a bad
+developer experience. The next time a new commit is pushed to the branch, the
+cache will be re-used.
+
+To enable per-job and per-branch caching:
+
+```yaml
+cache:
+ key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
+```
+
+To enable per-branch and per-stage caching:
+
+```yaml
+cache:
+ key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
+```
+
+### Sharing caches across different branches
+
+If the files you are caching need to be shared across all branches and all jobs,
+you can use the same key for all of them:
+
+```yaml
+cache:
+ key: one-key-to-rull-them-all
+```
+
+To share the same cache between branches, but separate them by job:
+
+```yaml
+cache:
+ key: ${CI_JOB_NAME}
+```
+
+### Disabling cache on specific jobs
+
+If you have defined the cache globally, it means that each job will use the
+same definition. You can override this behavior per-job, and if you want to
+disable it completely, use an empty hash:
+
+```yaml
+job:
+ cache: {}
+```
+
+For more fine tuning, read also about the
+[`cache: policy`](../yaml/README.md#cache-policy).
+
+## Common use cases
+
+The most common use case of cache is to preserve contents between subsequent
+runs of jobs for things like dependencies and commonly used libraries
+(Nodejs packages, PHP packages, rubygems, python libraries, etc.),
+so they don't have to be re-fetched from the public internet.
+
+NOTE: **Note:**
+For more examples, check the [GitLab CI Yml](https://gitlab.com/gitlab-org/gitlab-ci-yml)
+project.
+
+### Caching Nodejs dependencies
+
+Assuming your project is using [npm](https://www.npmjs.com/) or
+[Yarn](https://yarnpkg.com/en/) to install the Nodejs dependencies, the
+following example defines `cache` globally so that all jobs inherit it.
+Nodejs modules are installed in `node_modules/` and are cached per-branch:
+
+```yaml
+#
+# https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Nodejs.gitlab-ci.yml
+#
+image: node:latest
+
+# Cache modules in between jobs
+cache:
+ key: ${CI_COMMIT_REF_SLUG}
+ paths:
+ - node_modules/
+
+before_script:
+ - npm install
+
+test_async:
+ script:
+ - node ./specs/start.js ./specs/async.spec.js
+```
+
+### Caching PHP dependencies
+
+Assuming your project is using [Composer](https://getcomposer.org/) to install
+the PHP dependencies, the following example defines `cache` globally so that
+all jobs inherit it. PHP libraries modules are installed in `vendor/` and
+are cached per-branch:
+
+```yaml
+#
+# https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/PHP.gitlab-ci.yml
+#
+image: php:7.2
+
+# Cache libraries in between jobs
+cache:
+ key: ${CI_COMMIT_REF_SLUG}
+ paths:
+ - vendor/
+
+before_script:
+# Install and run Composer
+- curl --show-error --silent https://getcomposer.org/installer | php
+- php composer.phar install
+
+test:
+ script:
+ - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never
+```
+
+### Caching Python dependencies
+
+Assuming your project is using [pip](https://pip.pypa.io/en/stable/) to install
+the python dependencies, the following example defines `cache` globally so that
+all jobs inherit it. Python libraries are installed in a virtualenv under `venv/`,
+pip's cache is defined under `.cache/pip/` and both are cached per-branch:
+
+```yaml
+#
+# https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Python.gitlab-ci.yml
+#
+image: python:latest
+
+# Change pip's cache directory to be inside the project directory since we can
+# only cache local items.
+variables:
+ PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache"
+
+# Pip's cache doesn't store the python packages
+# https://pip.pypa.io/en/stable/reference/pip_install/#caching
+#
+# If you want to also cache the installed packages, you have to install
+# them in a virtualenv and cache it as well.
+cache:
+ paths:
+ - .cache/
+ - venv/
+
+before_script:
+ - python -V # Print out python version for debugging
+ - pip install virtualenv
+ - virtualenv venv
+ - source venv/bin/activate
+
+test:
+ script:
+ - python setup.py test
+ - pip install flake8
+ - flake8 .
+```
+
+### Caching Ruby dependencies
+
+Assuming your project is using [Bundler](https://bundler.io) to install the
+gem dependencies, the following example defines `cache` globally so that all
+jobs inherit it. Gems are installed in `vendor/ruby/` and are cached per-branch:
+
+```yaml
+#
+# https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Ruby.gitlab-ci.yml
+#
+image: ruby:2.5
+
+# Cache gems in between builds
+cache:
+ key: ${CI_COMMIT_REF_SLUG}
+ paths:
+ - vendor/ruby
+
+before_script:
+ - ruby -v # Print out ruby version for debugging
+ - gem install bundler --no-ri --no-rdoc # Bundler is not installed with the image
+ - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby
+
+rspec:
+ script:
+ - rspec spec
+```
+
+## Availability of the cache
+
+Caching is an optimization, but isn't guaranteed to always work, so you need to
+be prepared to regenerate any cached files in each job that needs them.
+
+Assuming you have properly [defined `cache` in `.gitlab-ci.yml`](../yaml/README.md#cache)
+according to your workflow, the availability of the cache ultimately depends on
+how the Runner has been configured (the executor type and whether different
+Runners are used for passing the cache between jobs).
+
+### Where the caches are stored
+
+Since the Runner is the one responsible for storing the cache, it's essential
+to know **where** it's stored. All the cache paths defined under a job in
+`.gitlab-ci.yml` are archived in a single `cache.zip` file and stored in the
+Runner's configured cache location. By default, they are stored locally in the
+machine where the Runner is installed and depends on the type of the executor.
+
+| GitLab Runner executor | Default path of the cache |
+| ---------------------- | ------------------------- |
+| [Shell](https://docs.gitlab.com/runner/executors/shell.html) | Locally, stored under the `gitlab-runner` user's home directory: `/home/gitlab-runner/cache/<user>/<project>/<cache-key>/cache.zip`. |
+| [Docker](https://docs.gitlab.com/runner/executors/docker.html) | Locally, stored under [Docker volumes](https://docs.gitlab.com/runner/executors/docker.html#the-builds-and-cache-storage): `/var/lib/docker/volumes/<volume-id>/_data/<user>/<project>/<cache-key>/cache.zip`. |
+| [Docker machine](https://docs.gitlab.com/runner/executors/docker_machine.html) (autoscale Runners) | Behaves the same as the Docker executor. |
+
+### How archiving and extracting works
+
+In the most simple scenario, consider that you use only one machine where the
+Runner is installed, and all jobs of your project run on the same host.
+
+Let's see the following example of two jobs that belong to two consecutive
+stages:
+
+```yaml
+stages:
+- build
+- test
+
+before_script:
+- echo "Hello"
+
+job A:
+ stage: build
+ script:
+ - mkdir vendor/
+ - echo "build" > vendor/hello.txt
+ cache:
+ key: build-cache
+ paths:
+ - vendor/
+ after_script:
+ - echo "World"
+
+job B:
+ stage: test
+ script:
+ - cat vendor/hello.txt
+ cache:
+ key: build-cache
+```
+
+Here's what happens behind the scenes:
+
+1. Pipeline starts
+1. `job A` runs
+1. `before_script` is executed
+1. `script` is executed
+1. `after_script` is executed
+1. `cache` runs and the `vendor/` directory is zipped into `cache.zip`.
+ This file is then saved in the directory based on the
+ [Runner's setting](#where-the-caches-are-stored) and the `cache: key`.
+1. `job B` runs
+1. The cache is extracted (if found)
+1. `before_script` is executed
+1. `script` is executed
+1. Pipeline finishes
+
+By using a single Runner on a single machine, you'll not have the issue where
+`job B` might execute on a Runner different from `job A`, thus guaranteeing the
+cache between stages. That will only work if the build goes from stage `build`
+to `test` in the same Runner/machine, otherwise, you [might not have the cache
+available](#cache-mismatch).
+
+During the caching process, there's also a couple of things to consider:
+
+- If some other job, with another cache configuration had saved its
+ cache in the same zip file, it is overwritten. If the S3 based shared cache is
+ used, the file is additionally uploaded to S3 to an object based on the cache
+ key. So, two jobs with different paths, but the same cache key, will overwrite
+ their cache.
+- When extracting the cache from `cache.zip`, everything in the zip file is
+ extracted in the job's working directory (usually the repository which is
+ pulled down), and the Runner doesn't mind if the archive of `job A` overwrites
+ things in the archive of `job B`.
+
+The reason why it works this way is because the cache created for one Runner
+often will not be valid when used by a different one which can run on a
+**different architecture** (e.g., when the cache includes binary files). And
+since the different steps might be executed by Runners running on different
+machines, it is a safe default.
+
+### Cache mismatch
+
+In the following table, you can see some reasons where you might hit a cache
+mismatch and a few ideas how to fix it.
+
+| Reason of a cache mismatch | How to fix it |
+| -------------------------- | ------------- |
+| You use multiple standalone Runners (not in autoscale mode) attached to one project without a shared cache | Use only one Runner for your project or use multiple Runners with distributed cache enabled |
+| You use Runners in autoscale mode without a distributed cache enabled | Configure the autoscale Runner to use a distributed cache |
+| The machine the Runner is installed on is low on disk space or, if you've set up distributed cache, the S3 bucket where the cache is stored doesn't have enough space | Make sure you clear some space to allow new caches to be stored. Currently, there's no automatic way to do this. |
+| You use the same `key` for jobs where they cache different paths. | Use different cache keys to that the cache archive is stored to a different location and doesn't overwrite wrong caches. |
+
+Let's explore some examples.
+
+---
+
+Let's assume you have only one Runner assigned to your project, so the cache
+will be stored in the Runner's machine by default. If two jobs, A and B,
+have the same cache key, but they cache different paths, cache B would overwrite
+cache A, even if their `paths` don't match:
+
+We want `job A` and `job B` to re-use their
+cache when the pipeline is run for a second time.
+
+```yaml
+stages:
+- build
+- test
+
+job A:
+ stage: build
+ script: make build
+ cache:
+ key: same-key
+ paths:
+ - public/
+
+job B:
+ stage: test
+ script: make test
+ cache:
+ key: same-key
+ paths:
+ - vendor/
+```
+
+1. `job A` runs
+1. `public/` is cached as cache.zip
+1. `job B` runs
+1. The previous cache, if any, is unzipped
+1. `vendor/` is cached as cache.zip and overwrites the previous one
+1. The next time `job A` runs it will use the cache of `job B` which is different
+ and thus will be ineffective
+
+To fix that, use different `keys` for each job.
+
+---
+
+In another case, let's assume you have more than one Runners assigned to your
+project, but the distributed cache is not enabled. We want the second time the
+pipeline is run, `job A` and `job B` to re-use their cache (which in this case
+will be different):
+
+```yaml
+stages:
+- build
+- test
+
+job A:
+ stage: build
+ script: build
+ cache:
+ key: keyA
+ paths:
+ - vendor/
+
+job B:
+ stage: test
+ script: test
+ cache:
+ key: keyB
+ paths:
+ - vendor/
+```
+
+In that case, even if the `key` is different (no fear of overwriting), you
+might experience the cached files to "get cleaned" before each stage if the
+jobs run on different Runners in the subsequent pipelines.
+
+## Clearing the cache
+
+GitLab Runners use [cache](../yaml/README.md#cache) to speed up the execution
+of your jobs by reusing existing data. This however, can sometimes lead to an
+inconsistent behavior.
+
+To start with a fresh copy of the cache, there are two ways to do that.
+
+### Clearing the cache by changing `cache:key`
+
+All you have to do is set a new `cache: key` in your `.gitlab-ci.yml`. In the
+next run of the pipeline, the cache will be stored in a different location.
+
+### Clearing the cache manually
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41249) in GitLab 10.4.
+
+If you want to avoid editing `.gitlab-ci.yml`, you can easily clear the cache
+via GitLab's UI:
+
+1. Navigate to your project's **CI/CD > Pipelines** page
+1. Click on the **Clear Runner caches** button to clean up the cache
+
+ ![Clear Runners cache](img/clear_runners_cache.png)
+
+1. On the next push, your CI/CD job will use a new cache
+
+Behind the scenes, this works by increasing a counter in the database, and the
+value of that counter is used to create the key for the cache by appending an
+integer to it: `-1`, `-2`, etc. After a push, a new key is generated and the
+old cache is not valid anymore.
+
+## Cache vs artifacts
+
+NOTE: **Note:**
+Be careful if you use cache and artifacts to store the same path in your jobs
+as **caches are restored before artifacts** and the content would be overwritten.
+
+Don't mix the caching with passing artifacts between stages. Caching is not
+designed to pass artifacts between stages. Cache is for runtime dependencies
+needed to compile the project:
+
+- `cache` - **Use for temporary storage for project dependencies.** Not useful
+ for keeping intermediate build results, like `jar` or `apk` files.
+ Cache was designed to be used to speed up invocations of subsequent runs of a
+ given job, by keeping things like dependencies (e.g., npm packages, Go vendor
+ packages, etc.) so they don't have to be re-fetched from the public internet.
+ While the cache can be abused to pass intermediate build results between stages,
+ there may be cases where artifacts are a better fit.
+- `artifacts` - **Use for stage results that will be passed between stages.**
+ Artifacts were designed to upload some compiled/generated bits of the build,
+ and they can be fetched by any number of concurrent Runners. They are
+ guaranteed to be available and are there to pass data between jobs. They are
+ also exposed to be downloaded from the UI.
+
+It's sometimes confusing because the name artifact sounds like something that
+is only useful outside of the job, like for downloading a final image. But
+artifacts are also available in between stages within a pipeline. So if you
+build your application by downloading all the required modules, you might want
+to declare them as artifacts so that each subsequent stage can depend on them
+being there. There are some optimizations like declaring an
+[expiry time](../yaml/README.md#artifacts-expire_in) so you don't keep artifacts
+around too long, and using [dependencies](../yaml/README.md#dependencies) to
+control exactly where artifacts are passed around.
+
+So, to sum up:
+- Caches are disabled if not defined globally or per job (using `cache:`)
+- Caches are available for all jobs in your `.gitlab-ci.yml` if enabled globally
+- Caches can be used by subsequent pipelines of that very same job (a script in
+ a stage) in which the cache was created (if not defined globally).
+- Caches are stored where the Runner is installed **and** uploaded to S3 if
+ [distributed cache is enabled](https://docs.gitlab.com/runner/configuration/autoscale.html#distributed-runners-caching)
+- Caches defined per job are only used either a) for the next pipeline of that job,
+ or b) if that same cache is also defined in a subsequent job of the same pipeline
+- Artifacts are disabled if not defined per job (using `artifacts:`)
+- Artifacts can only be enabled per job, not globally
+- Artifacts are created during a pipeline and can be used by the subsequent
+ jobs of that currently active pipeline
+- Artifacts are always uploaded to GitLab (known as coordinator)
+- Artifacts can have an expiration value for controlling disk usage (30 days by default)
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 22afcb9199d..183808641c0 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -1,26 +1,29 @@
-# Using Docker Build
+# Building Docker images with GitLab CI/CD
-GitLab CI allows you to use Docker Engine to build and test docker-based projects.
+GitLab CI/CD allows you to use Docker Engine to build and test docker-based projects.
-**This also allows to you to use `docker-compose` and other docker-enabled tools.**
+TIP: **Tip:**
+This also allows to you to use `docker-compose` and other docker-enabled tools.
One of the new trends in Continuous Integration/Deployment is to:
-1. create an application image,
-1. run tests against the created image,
-1. push image to a remote registry, and
-1. deploy to a server from the pushed image.
+1. Create an application image
+1. Run tests against the created image
+1. Push image to a remote registry
+1. Deploy to a server from the pushed image
-It's also useful when your application already has the `Dockerfile` that can be used to create and test an image:
+It's also useful when your application already has the `Dockerfile` that can be
+used to create and test an image:
```bash
-$ docker build -t my-image dockerfiles/
-$ docker run my-docker-image /script/to/run/tests
-$ docker tag my-image my-registry:5000/my-image
-$ docker push my-registry:5000/my-image
+docker build -t my-image dockerfiles/
+docker run my-docker-image /script/to/run/tests
+docker tag my-image my-registry:5000/my-image
+docker push my-registry:5000/my-image
```
-This requires special configuration of GitLab Runner to enable `docker` support during jobs.
+This requires special configuration of GitLab Runner to enable `docker` support
+during jobs.
## Runner Configuration
@@ -74,8 +77,8 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user.
5. You can now use `docker` command and install `docker-compose` if needed.
-> **Note:**
-* By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions.
+NOTE: **Note:**
+By adding `gitlab-runner` to the `docker` group you are effectively granting `gitlab-runner` full root permissions.
For more information please read [On Docker security: `docker` group considered harmful](https://www.andreas-jung.com/contents/on-docker-security-docker-group-considered-harmful).
### Use docker-in-docker executor
@@ -259,8 +262,66 @@ aware of the following implications:
docker run --rm -t -i -v $(pwd)/src:/home/app/src test-image:latest run_app_tests
```
+## Making docker-in-docker builds faster with Docker layer caching
+
+When using docker-in-docker, Docker will download all layers of your image every
+time you create a build. Recent versions of Docker (Docker 1.13 and above) can
+use a pre-existing image as a cache during the `docker build` step, considerably
+speeding up the build process.
+
+### How Docker caching works
+
+When running `docker build`, each command in `Dockerfile` results in a layer.
+These layers are kept around as a cache and can be reused if there haven't been
+any changes. Change in one layer causes all subsequent layers to be recreated.
+
+You can specify a tagged image to be used as a cache source for the `docker build`
+command by using the `--cache-from` argument. Multiple images can be specified
+as a cache source by using multiple `--cache-from` arguments. Keep in mind that
+any image that's used with the `--cache-from` argument must first be pulled
+(using `docker pull`) before it can be used as a cache source.
+
+### Using Docker caching
+
+Here's a simple `.gitlab-ci.yml` file showing how Docker caching can be utilized:
+
+```yaml
+image: docker:latest
+
+services:
+ - docker:dind
+
+variables:
+ CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
+ DOCKER_DRIVER: overlay2
+
+before_script:
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
+
+build:
+ stage: build
+ script:
+ - docker pull $CONTAINER_IMAGE:latest || true
+ - docker build --cache-from $CONTAINER_IMAGE:latest --tag $CONTAINER_IMAGE:$CI_BUILD_REF --tag $CONTAINER_IMAGE:latest .
+ - docker push $CONTAINER_IMAGE:$CI_BUILD_REF
+ - docker push $CONTAINER_IMAGE:latest
+```
+
+The steps in the `script` section for the `build` stage can be summed up to:
+
+1. The first command tries to pull the image from the registry so that it can be
+ used as a cache for the `docker build` command.
+1. The second command builds a Docker image using the pulled image as a
+ cache (notice the `--cache-from $CONTAINER_IMAGE:latest` argument) if
+ available, and tags it.
+1. The last two commands push the tagged Docker images to the container registry
+ so that they may also be used as cache for subsequent builds.
+
## Using the OverlayFS driver
+NOTE: **Note:**
+The shared Runners on GitLab.com use the `overlay2` driver by default.
+
By default, when using `docker:dind`, Docker uses the `vfs` storage driver which
copies the filesystem on every run. This is a very disk-intensive operation
which can be avoided if a different driver is used, for example `overlay2`.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index c1e258aedca..de60cd27cd1 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -49,6 +49,10 @@ There's also a collection of repositories with [example projects](https://gitlab
**(Ultimate)** [Scan your code for vulnerabilities](https://docs.gitlab.com/ee/ci/examples/sast.html)
+## Dependency Scanning
+
+**(Ultimate)** [Scan your dependencies for vulnerabilities](https://docs.gitlab.com/ee/ci/examples/dependency_scanning.html)
+
## Container Scanning
[Scan your Docker images for vulnerabilities](container_scanning.md)
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index e504b81eae8..f64e868d390 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -104,8 +104,8 @@ Jobs are used to create jobs, which are then picked by
What is important is that each job is run independently from each other.
-If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
-Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
+If you want to check whether the `.gitlab-ci.yml` of your project is valid, there is a
+Lint tool under the page `/ci/lint` of your project namespace. You can also find
a "CI Lint" button to go to this page under **CI/CD âž” Pipelines** and
**Pipelines âž” Jobs** in your project.
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index f879ed62010..7a7b50b294d 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -146,24 +146,7 @@ To protect/unprotect Runners:
## Manually clearing the Runners cache
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41249) in GitLab 10.4.
-
-GitLab Runners use [cache](../yaml/README.md#cache) to speed up the execution
-of your jobs by reusing existing data. This however, can sometimes lead to an
-inconsistent behavior.
-
-To start with a fresh copy of the cache, you can easily do it via GitLab's UI:
-
-1. Navigate to your project's **CI/CD > Pipelines** page.
-1. Click on the **Clear Runner caches** to clean up the cache.
-1. On the next push, your CI/CD job will use a new cache.
-
-That way, you don't have to change the [cache key](../yaml/README.md#cache-key)
-in your `.gitlab-ci.yml`.
-
-Behind the scenes, this works by increasing a counter in the database, and the
-value of that counter is used to create the key for the cache. After a push, a
-new key is generated and the old cache is not valid anymore.
+Read [clearing the cache](../caching/index.md#clearing-the-cache).
## How shared Runners pick jobs
@@ -227,15 +210,16 @@ that it may encounter on the projects it's shared over. This would be
problematic for large amounts of projects, if it wasn't for tags.
By tagging a Runner for the types of jobs it can handle, you can make sure
-shared Runners will only run the jobs they are equipped to run.
+shared Runners will [only run the jobs they are equipped to run](../yaml/README.md#tags).
For instance, at GitLab we have Runners tagged with "rails" if they contain
the appropriate dependencies to run Rails test suites.
### Preventing Runners with tags from picking jobs without tags
-You can configure a Runner to prevent it from picking jobs with tags when
-the Runner does not have tags assigned. This setting can be enabled the first
+You can configure a Runner to prevent it from picking
+[jobs with tags](../yaml/README.md#tags) when the Runner does not have tags
+assigned. This setting can be enabled the first
time you [register a Runner][register] and can be changed afterwards under
each Runner's settings.
@@ -280,3 +264,36 @@ We're always looking for contributions that can mitigate these
[register]: http://docs.gitlab.com/runner/register/
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
+
+## Determining the IP address of a Runner
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17286) in GitLab 10.6.
+
+It may be useful to know the IP address of a Runner so you can troubleshoot
+issues with that Runner. GitLab stores and displays the IP address by viewing
+the source of the HTTP requests it makes to GitLab when polling for jobs. The
+IP address is always kept up to date so if the Runner IP changes it will be
+automatically updated in GitLab.
+
+The IP address for shared Runners and specific Runners can be found in
+different places.
+
+### Shared Runners
+
+To view the IP address of a shared Runner you must have admin access to
+the GitLab instance. To determine this:
+
+1. Visit **Admin area âž” Overview âž” Runners**
+1. Look for the Runner in the table and you should see a column for "IP Address"
+
+![shared Runner IP address](img/shared_runner_ip_address.png)
+
+### Specific Runners
+
+You can find the IP address of a Runner for a specific project by:
+
+1. Visit your project's **Settings âž” CI/CD**
+1. Find the Runner and click on it's ID which links you to the details page
+1. On the details page you should see a row for "IP Address"
+
+![specific Runner IP address](img/specific_runner_ip_address.png)
diff --git a/doc/ci/runners/img/shared_runner_ip_address.png b/doc/ci/runners/img/shared_runner_ip_address.png
new file mode 100644
index 00000000000..3b1542d59d3
--- /dev/null
+++ b/doc/ci/runners/img/shared_runner_ip_address.png
Binary files differ
diff --git a/doc/ci/runners/img/specific_runner_ip_address.png b/doc/ci/runners/img/specific_runner_ip_address.png
new file mode 100644
index 00000000000..3b4c3e9f2eb
--- /dev/null
+++ b/doc/ci/runners/img/specific_runner_ip_address.png
Binary files differ
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index accf6340398..c2b06e53c2f 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -674,6 +674,10 @@ as Review Apps. You can see a simple example using Review Apps at
by default.
- From GitLab 9.2, caches are restored before [artifacts](#artifacts).
+TIP: **Learn more:**
+Read how caching works and find out some good practices in the
+[caching dependencies documentation](../caching/index.md).
+
`cache` is used to specify a list of files and directories which should be
cached between jobs. You can only use paths that are within the project
workspace.
@@ -681,35 +685,20 @@ workspace.
If `cache` is defined outside the scope of jobs, it means it is set
globally and all jobs will use that definition.
-Cache all files in `binaries` and `.config`:
-
-```yaml
-rspec:
- script: test
- cache:
- paths:
- - binaries/
- - .config
-```
-
-Cache all Git untracked files:
+### `cache:paths`
-```yaml
-rspec:
- script: test
- cache:
- untracked: true
-```
+Use the `paths` directive to choose which files or directories will be cached.
+Wildcards can be used as well.
-Cache all Git untracked files and files in `binaries`:
+Cache all files in `binaries` that end in `.apk` and the `.config` file:
```yaml
rspec:
script: test
cache:
- untracked: true
paths:
- - binaries/
+ - binaries/*.apk
+ - .config
```
Locally defined cache overrides globally defined options. The following `rspec`
@@ -723,33 +712,26 @@ cache:
rspec:
script: test
cache:
- key: rspec
paths:
- binaries/
```
-Note that since cache is shared between jobs, if you're using different
-paths for different jobs, you should also set a different **cache:key**
-otherwise cache content can be overwritten.
-
-NOTE: **Note:**
-The cache is provided on a best-effort basis, so don't expect that the cache
-will be always present.
-
### `cache:key`
> Introduced in GitLab Runner v1.0.0.
-The `key` directive allows you to define the affinity of caching
-between jobs, allowing to have a single cache for all jobs,
-cache per-job, cache per-branch or any other way that fits your needs.
+Since the cache is shared between jobs, if you're using different
+paths for different jobs, you should also set a different `cache:key`
+otherwise cache content can be overwritten.
-This way, you can fine tune caching, allowing you to cache data between
-different jobs or even different branches.
+The `key` directive allows you to define the affinity of caching between jobs,
+allowing to have a single cache for all jobs, cache per-job, cache per-branch
+or any other way that fits your workflow. This way, you can fine tune caching,
+allowing you to cache data between different jobs or even different branches.
The `cache:key` variable can use any of the
[predefined variables](../variables/README.md), and the default key, if not set,
-is set as `$CI_JOB_NAME-$CI_COMMIT_REF_NAME` which translates as "per-job and
+is `$CI_JOB_NAME-$CI_COMMIT_REF_NAME` which translates as "per-job and
per-branch". It is the default across the project, therefore everything is
shared between pipelines and jobs running on the same branch by default.
@@ -757,56 +739,56 @@ NOTE: **Note:**
The `cache:key` variable cannot contain the `/` character, or the equivalent
URI-encoded `%2F`; a value made only of dots (`.`, `%2E`) is also forbidden.
-**Example configurations**
-
-To enable per-job caching:
-
-```yaml
-cache:
- key: "$CI_JOB_NAME"
- untracked: true
-```
-
-To enable per-branch caching:
+For example, to enable per-branch caching:
```yaml
cache:
key: "$CI_COMMIT_REF_SLUG"
- untracked: true
+ paths:
+ - binaries/
```
-To enable per-job and per-branch caching:
+If you use **Windows Batch** to run your shell scripts you need to replace
+`$` with `%`:
```yaml
cache:
- key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
- untracked: true
+ key: "%CI_JOB_STAGE%-%CI_COMMIT_REF_SLUG%"
+ paths:
+ - binaries/
```
-To enable per-branch and per-stage caching:
+If you use **Windows PowerShell** to run your shell scripts you need to replace
+`$` with `$env:`:
```yaml
cache:
- key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
- untracked: true
+ key: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_SLUG"
+ paths:
+ - binaries/
```
-If you use **Windows Batch** to run your shell scripts you need to replace
-`$` with `%`:
+### `cache:untracked`
+
+Set `untracked: true` to cache all files that are untracked in your Git
+repository:
```yaml
-cache:
- key: "%CI_JOB_STAGE%-%CI_COMMIT_REF_SLUG%"
- untracked: true
+rspec:
+ script: test
+ cache:
+ untracked: true
```
-If you use **Windows PowerShell** to run your shell scripts you need to replace
-`$` with `$env:`:
+Cache all Git untracked files and files in `binaries`:
```yaml
-cache:
- key: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_SLUG"
- untracked: true
+rspec:
+ script: test
+ cache:
+ untracked: true
+ paths:
+ - binaries/
```
### `cache:policy`
@@ -1150,7 +1132,7 @@ job1:
## `retry`
-> [Introduced][ce-3442] in GitLab 9.5.
+> [Introduced][ce-12909] in GitLab 9.5.
`retry` allows you to configure how many times a job is going to be retried in
case of a failure.
@@ -1544,8 +1526,9 @@ capitalization, the commit will be created but the pipeline will be skipped.
## Validate the .gitlab-ci.yml
-Each instance of GitLab CI has an embedded debug tool called Lint.
-You can find the link under `/ci/lint` of your gitlab instance.
+Each instance of GitLab CI has an embedded debug tool called Lint, which validates the
+content of your `.gitlab-ci.yml` files. You can find the Lint under the page `ci/lint` of your
+project namespace (e.g, `http://gitlab-example.com/gitlab-org/project-123/ci/lint`)
## Using reserved keywords
@@ -1565,5 +1548,5 @@ CI with various languages.
[variables]: ../variables/README.md
[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983
[ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447
-[ce-3442]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3442
+[ce-12909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12909
[schedules]: ../../user/project/pipelines/schedules.md
diff --git a/doc/development/emails.md b/doc/development/emails.md
index 677029b1295..4dbf064fd75 100644
--- a/doc/development/emails.md
+++ b/doc/development/emails.md
@@ -60,16 +60,10 @@ See the [Rails guides] for more info.
As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
-1. Uncomment the `mail_room` line in your `Procfile`:
-
- ```yaml
- mail_room: bundle exec mail_room -q -c config/mail_room.yml
- ```
-
-1. Restart GitLab:
+1. Run this command in the GitLab root directory to launch `mail_room`:
```sh
- bundle exec foreman start
+ bundle exec mail_room -q -c config/mail_room.yml
```
1. Verify that everything is configured correctly:
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index 960eabd5538..cf62314bc29 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -10,6 +10,7 @@ are very appreciative of the work done by translators and proofreaders!
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Chinese Traditional
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
+ - Weizhe Ding - [GitLab](https://gitlab.com/d.weizhe), [Crowdin](https://crowdin.com/profile/d.weizhe)
- Chinese Traditional, Hong Kong
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Dutch
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 243ac7f0c98..a211effdfa7 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -23,10 +23,6 @@ When downtime is necessary the migration has to be approved by:
An up-to-date list of people holding these titles can be found at
<https://about.gitlab.com/team/>.
-The document ["What Requires Downtime?"](what_requires_downtime.md) specifies
-various database operations, whether they require downtime and how to
-work around that whenever possible.
-
When writing your migrations, also consider that databases might have stale data
or inconsistencies and guard for that. Try to make as few assumptions as
possible about the state of the database.
@@ -41,6 +37,18 @@ Migrations that make changes to the database schema (e.g. adding a column) can
only be added in the monthly release, patch releases may only contain data
migrations _unless_ schema changes are absolutely required to solve a problem.
+## What Requires Downtime?
+
+The document ["What Requires Downtime?"](what_requires_downtime.md) specifies
+various database operations, such as
+
+- [adding, dropping, and renaming columns](what_requires_downtime.md#adding-columns)
+- [changing column constraints and types](what_requires_downtime.md#changing-column-constraints)
+- [adding and dropping indexes, tables, and foreign keys](what_requires_downtime.md#adding-indexes)
+
+and whether they require downtime and how to work around that whenever possible.
+
+
## Downtime Tagging
Every migration must specify if it requires downtime or not, and if it should
@@ -136,11 +144,14 @@ class MyMigration < ActiveRecord::Migration
disable_ddl_transaction!
def up
- remove_concurrent_index :table_name, :column_name if index_exists?(:table_name, :column_name)
+ remove_concurrent_index :table_name, :column_name
end
end
```
+Note that it is not necessary to check if the index exists prior to
+removing it.
+
## Adding indexes
If you need to add a unique index please keep in mind there is the possibility
diff --git a/doc/development/new_fe_guide/development/performance.md b/doc/development/new_fe_guide/development/performance.md
index 26b07874f0f..244dfb3756f 100644
--- a/doc/development/new_fe_guide/development/performance.md
+++ b/doc/development/new_fe_guide/development/performance.md
@@ -1,3 +1,16 @@
# Performance
-> TODO: Add content
+## Monitoring
+
+We have a performance dashboard available in one of our [grafana instances](https://performance.gprd.gitlab.com/dashboard/db/sitespeed-page-summary?orgId=1). This dashboard automatically aggregates metric data from [sitespeed.io](https://sitespeed.io) every 6 hours. These changes are displayed after a set number of pages are aggregated.
+
+These pages can be found inside a text file in the gitlab-build-images [repository](https://gitlab.com/gitlab-org/gitlab-build-images) called [gitlab.txt](https://gitlab.com/gitlab-org/gitlab-build-images/blob/master/scripts/gitlab.txt)
+Any frontend engineer can contribute to this dashboard. They can contribute by adding or removing urls of pages from this text file. Please have a [frontend monitoring expert](https://about.gitlab.com/team) review your changes before assigning to a maintainer of the `gitlab-build-images` project. The changes will go live on the next scheduled run after the changes are merged into `master`.
+
+There are 3 recommended high impact metrics to review on each page
+
+* [First visual change](https://developers.google.com/web/tools/lighthouse/audits/first-meaningful-paint)
+* [Speed Index](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index)
+* [Visual Complete 95%](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index)
+
+For these metrics, lower numbers are better as it means that the website is more performant.
diff --git a/doc/img/devops_lifecycle.png b/doc/img/devops_lifecycle.png
new file mode 100644
index 00000000000..0616be46df8
--- /dev/null
+++ b/doc/img/devops_lifecycle.png
Binary files differ
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index cd889e74487..aa9b8777359 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -10,7 +10,7 @@ should be deployed, upgraded, and configured.
## Chart Overview
* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart).
-* **[Cloud Native GitLab Chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)**: The next generation GitLab chart, currently in development. Will support large deployments with horizontal scaling of individual GitLab components.
+* **[Cloud Native GitLab Chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)**: The next generation GitLab chart, currently in alpha. Will support large deployments with horizontal scaling of individual GitLab components.
* Other Charts
* [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner.
* [Advanced GitLab Installation](gitlab_chart.md): Deprecated, being replaced by the [cloud native GitLab chart](#cloud-native-gitlab-chart). Provides additional deployment options, but provides less functionality out-of-the-box.
@@ -35,9 +35,9 @@ By offering individual containers and charts, we will be able to provide a numbe
* Potential for rolling updates and canaries within a service,
* and plenty more.
-This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We are planning to launch this chart in beta by the end of 2017.
+Presently this chart is available in alpha for testing, and not recommended for production use.
-Learn more about the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md).
+Learn more about the [cloud native GitLab chart here ](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8).
## Other Charts
diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md
index c39d7ab57c6..a75836a915a 100644
--- a/doc/integration/auth0.md
+++ b/doc/integration/auth0.md
@@ -56,7 +56,8 @@ for initial settings.
"name" => "auth0",
"args" => { client_id: 'YOUR_AUTH0_CLIENT_ID',
client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
- namespace: 'YOUR_AUTH0_DOMAIN'
+ domain: 'YOUR_AUTH0_DOMAIN',
+ scope: 'openid profile email'
}
}
]
@@ -69,8 +70,8 @@ for initial settings.
args: {
client_id: 'YOUR_AUTH0_CLIENT_ID',
client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
- namespace: 'YOUR_AUTH0_DOMAIN'
- }
+ domain: 'YOUR_AUTH0_DOMAIN',
+ scope: 'openid profile email' }
}
```
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 20087a981f9..3edde3de83d 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -32,6 +32,7 @@ contains some settings that are common for all providers.
- [Auth0](auth0.md)
- [Authentiq](../administration/auth/authentiq.md)
- [OAuth2Generic](oauth2_generic.md)
+- [JWT](../administration/auth/jwt.md)
## Initial OmniAuth Configuration
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index f8a7dd6b1dc..3f49432ce93 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -102,9 +102,10 @@ in your SAML IdP:
installation to generate the correct value).
1. Change the values of `idp_cert_fingerprint`, `idp_sso_target_url`,
- `name_identifier_format` to match your IdP. Check
+ `name_identifier_format` to match your IdP. If a fingerprint is used it must
+ be a SHA1 fingerprint; check
[the omniauth-saml documentation](https://github.com/omniauth/omniauth-saml)
- for details on these options.
+ for more details on these options.
1. Change the value of `issuer` to a unique name, which will identify the application
to the IdP.
@@ -311,6 +312,7 @@ need to be validated using a fingerprint, a certificate or a validator.
For this you need take the following into account:
+- If a fingerprint is used, it must be the SHA1 fingerprint
- If no certificate is provided in the settings, a fingerprint or fingerprint
validator needs to be provided and the response from the server must contain
a certificate (`<ds:KeyInfo><ds:X509Data><ds:X509Certificate>`)
diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md
index 2f916f5dea7..90187617c41 100644
--- a/doc/raketasks/README.md
+++ b/doc/raketasks/README.md
@@ -14,3 +14,4 @@ comments: false
- [Webhooks](web_hooks.md)
- [Import](import.md) of git repositories in bulk
- [Rebuild authorized_keys file](http://docs.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators
+- [Migrate Uploads](../administration/raketasks/uploads/migrate.md)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 4dc3adc1441..e88b787187c 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -20,6 +20,7 @@ project in an easy and automatic way:
1. [Auto Test](#auto-test)
1. [Auto Code Quality](#auto-code-quality)
1. [Auto SAST (Static Application Security Testing)](#auto-sast)
+1. [Auto Dependency Scanning](#auto-dependency-scanning)
1. [Auto Container Scanning](#auto-container-scanning)
1. [Auto Review Apps](#auto-review-apps)
1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast)
@@ -95,7 +96,7 @@ Auto Deploy, and Auto Monitoring will be silently skipped.
The Auto DevOps base domain is required if you want to make use of [Auto
Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined
-either under the project's CI/CD settings while
+either under the project's CI/CD settings while
[enabling Auto DevOps](#enabling-auto-devops) or in instance-wide settings in
the CI/CD section.
It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`.
@@ -209,7 +210,7 @@ target branches are also
> Introduced in [GitLab Ultimate][ee] 10.3.
Static Application Security Testing (SAST) uses the
-[gl-sast Docker image](https://gitlab.com/gitlab-org/gl-sast) to run static
+[SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast) to run static
analysis on the current code and checks for potential security issues. Once the
report is created, it's uploaded as an artifact which you can later download and
check out.
@@ -217,6 +218,19 @@ check out.
In GitLab Ultimate, any security warnings are also
[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html).
+### Auto Dependency Scanning
+
+> Introduced in [GitLab Ultimate][ee] 10.7.
+
+Dependency Scanning uses the
+[Dependency Scanning Docker image](https://gitlab.com/gitlab-org/security-products/dependency-scanning)
+to run analysis on the project dependencies and checks for potential security issues. Once the
+report is created, it's uploaded as an artifact which you can later download and
+check out.
+
+In GitLab Ultimate, any security warnings are also
+[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html).
+
### Auto Container Scanning
> Introduced in GitLab 10.4.
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
new file mode 100644
index 00000000000..83eb7a225b2
--- /dev/null
+++ b/doc/user/gitlab_com/index.md
@@ -0,0 +1,262 @@
+# GitLab.com settings
+
+In this page you will find information about the settings that are used on
+[GitLab.com](https://about.gitlab.com/pricing).
+
+## SSH host keys fingerprints
+
+Below are the fingerprints for GitLab.com's SSH host keys.
+
+| Algorithm | MD5 | SHA256 |
+| --------- | --- | ------- |
+| DSA | `7a:47:81:3a:ee:89:89:64:33:ca:44:52:3d:30:d4:87` | `p8vZBUOR0XQz6sYiaWSMLmh0t9i8srqYKool/Xfdfqw` |
+| ECDSA | `f1:d0:fb:46:73:7a:70:92:5a:ab:5d:ef:43:e2:1c:35` | `HbW3g8zUjNSksFbqTiUWPWg2Bq1x8xdGUrliXFzSnUw` |
+| ED25519 | `2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16` | `eUXGGm1YGsMAS7vkcx6JOJdOGHPem5gQp4taiCfCLB8` |
+| RSA | `b6:03:0e:39:97:9e:d0:e7:24:ce:a3:77:3e:01:42:09` | `ROQFvPThGrW4RuWLoL9tq9I9zJ42fK4XywyRtbOz/EQ` |
+
+## Mail configuration
+
+GitLab.com sends emails from the `mg.gitlab.com` domain via [Mailgun] and has
+its own dedicated IP address (`198.61.254.240`).
+
+## Alternative SSH port
+
+GitLab.com can be reached via a [different SSH port][altssh] for `git+ssh`.
+
+| Setting | Value |
+| --------- | ------------------- |
+| `Hostname` | `altssh.gitlab.com` |
+| `Port` | `443` |
+
+An example `~/.ssh/config` is the following:
+
+```
+Host gitlab.com
+ Hostname altssh.gitlab.com
+ User git
+ Port 443
+ PreferredAuthentications publickey
+ IdentityFile ~/.ssh/gitlab
+```
+
+## GitLab Pages
+
+Below are the settings for [GitLab Pages].
+
+| Setting | GitLab.com | Default |
+| ----------------------- | ---------------- | ------------- |
+| Domain name | `gitlab.io` | - |
+| IP address | `52.167.214.135` | - |
+| Custom domains support | yes | no |
+| TLS certificates support| yes | no |
+
+The maximum size of your Pages site is regulated by the artifacts maximum size
+which is part of [GitLab CI](#gitlab-ci).
+
+## GitLab CI/CD
+
+Below are the current settings regarding [GitLab CI/CD](../../ci/README.md).
+
+| Setting | GitLab.com | Default |
+| ----------- | ----------------- | ------------- |
+| Artifacts maximum size | 1G | 100M |
+
+## Shared Runners
+
+Shared Runners on GitLab.com run in [autoscale mode] and powered by
+DigitalOcean. Autoscaling means reduced waiting times to spin up builds,
+and isolated VMs for each project, thus maximizing security.
+
+They're free to use for public open source projects and limited to 2000 CI
+minutes per month per group for private projects. Read about all
+[GitLab.com plans](https://about.gitlab.com/pricing/).
+
+All your builds run on 2GB (RAM) ephemeral instances, with CoreOS and the latest
+Docker Engine installed. The default region of the VMs is NYC.
+
+Below are the shared Runners settings.
+
+| Setting | GitLab.com | Default |
+| ----------- | ----------------- | ---------- |
+| [GitLab Runner] | [Runner versions dashboard][ci_version_dashboard] | - |
+| Executor | `docker+machine` | - |
+| Default Docker image | `ruby:2.1` | - |
+| `privileged` (run [Docker in Docker]) | `true` | `false` |
+
+[ci_version_dashboard]: https://monitor.gitlab.net/dashboard/db/ci?refresh=5m&orgId=1&panelId=12&fullscreen&from=now-1h&to=now&var-runner_type=All&var-cache_server=All&var-gl_monitor_fqdn=postgres-01.db.prd.gitlab.com&var-has_minutes=yes&var-hanging_droplets_cleaner=All&var-droplet_zero_machines_cleaner=All&var-runner_job_failure_reason=All&theme=light
+
+### `config.toml`
+
+The full contents of our `config.toml` are:
+
+```toml
+[[runners]]
+ name = "docker-auto-scale"
+ limit = X
+ request_concurrency = X
+ url = "https://gitlab.com/ci"
+ token = "SHARED_RUNNER_TOKEN"
+ executor = "docker+machine"
+ environment = [
+ "DOCKER_DRIVER=overlay2"
+ ]
+ [runners.docker]
+ image = "ruby:2.1"
+ privileged = true
+ [runners.machine]
+ IdleCount = 40
+ IdleTime = 1800
+ MaxBuilds = 1
+ MachineDriver = "digitalocean"
+ MachineName = "machine-%s-digital-ocean-2gb"
+ MachineOptions = [
+ "digitalocean-image=coreos-stable",
+ "digitalocean-ssh-user=core",
+ "digitalocean-access-token=DIGITAL_OCEAN_ACCESS_TOKEN",
+ "digitalocean-region=nyc1",
+ "digitalocean-size=2gb",
+ "digitalocean-private-networking",
+ "digitalocean-userdata=/etc/gitlab-runner/cloudinit.sh",
+ "engine-registry-mirror=http://IP_TO_OUR_REGISTRY_MIRROR"
+ ]
+ [runners.cache]
+ Type = "s3"
+ ServerAddress = "IP_TO_OUR_CACHE_SERVER"
+ AccessKey = "ACCESS_KEY"
+ SecretKey = "ACCESS_SECRET_KEY"
+ BucketName = "runner"
+ Shared = true
+```
+
+## Sidekiq
+
+GitLab.com runs [Sidekiq][sidekiq] with arguments `--timeout=4 --concurrency=4`
+and the following environment variables:
+
+| Setting | GitLab.com | Default |
+|-------- |----------- |-------- |
+| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `1000000` | `1000000` |
+| `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL` | `SIGKILL` | - |
+| `SIDEKIQ_LOG_ARGUMENTS` | `1` | - |
+
+## Cron jobs
+
+Periodically executed jobs by Sidekiq, to self-heal Gitlab, do external
+synchronizations, run scheduled pipelines, etc.:
+
+| Setting | GitLab.com | Default |
+|-------- |------------- |------------- |
+| `pipeline_schedule_worker` | `19 * * * *` | `19 * * * *` |
+
+## PostgreSQL
+
+GitLab.com being a fairly large installation of GitLab means we have changed
+various PostgreSQL settings to better suit our needs. For example, we use
+streaming replication and servers in hot-standby mode to balance queries across
+different database servers.
+
+The list of GitLab.com specific settings (and their defaults) is as follows:
+
+| Setting | GitLab.com | Default |
+|:------------------------------------|:--------------------------------------------------------------------|:--------------------------------------|
+| archive_command | `/usr/bin/envdir /etc/wal-e.d/env /opt/wal-e/bin/wal-e wal-push %p` | empty |
+| archive_mode | on | off |
+| autovacuum_analyze_scale_factor | 0.01 | 0.01 |
+| autovacuum_max_workers | 6 | 3 |
+| autovacuum_vacuum_cost_limit | 1000 | -1 |
+| autovacuum_vacuum_scale_factor | 0.01 | 0.02 |
+| checkpoint_completion_target | 0.7 | 0.9 |
+| checkpoint_segments | 32 | 10 |
+| effective_cache_size | 338688MB | Based on how much memory is available |
+| hot_standby | on | off |
+| hot_standby_feedback | on | off |
+| log_autovacuum_min_duration | 0 | -1 |
+| log_checkpoints | on | off |
+| log_line_prefix | `%t [%p]: [%l-1] ` | empty |
+| log_min_duration_statement | 1000 | -1 |
+| log_temp_files | 0 | -1 |
+| maintenance_work_mem | 2048MB | 16 MB |
+| max_replication_slots | 5 | 0 |
+| max_wal_senders | 32 | 0 |
+| max_wal_size | 5GB | 1GB |
+| shared_buffers | 112896MB | Based on how much memory is available |
+| shared_preload_libraries | pg_stat_statements | empty |
+| shmall | 30146560 | Based on the server's capabilities |
+| shmmax | 123480309760 | Based on the server's capabilities |
+| wal_buffers | 16MB | -1 |
+| wal_keep_segments | 512 | 10 |
+| wal_level | replica | minimal |
+| statement_timeout | 15s | 60s |
+| idle_in_transaction_session_timeout | 60s | 60s |
+
+Some of these settings are in the process being adjusted. For example, the value
+for `shared_buffers` is quite high and as such we are looking into adjusting it.
+More information on this particular change can be found at
+<https://gitlab.com/gitlab-com/infrastructure/issues/1555>. An up to date list
+of proposed changes can be found at
+<https://gitlab.com/gitlab-com/infrastructure/issues?scope=all&utf8=%E2%9C%93&state=opened&label_name[]=database&label_name[]=change>.
+
+## Unicorn
+
+GitLab.com adjusts the memory limits for the [unicorn-worker-killer][unicorn-worker-killer] gem.
+
+Base default:
+* `memory_limit_min` = 750MiB
+* `memory_limit_max` = 1024MiB
+
+Web front-ends:
+* `memory_limit_min` = 1024MiB
+* `memory_limit_max` = 1280MiB
+
+## GitLab.com at scale
+
+In addition to the GitLab Enterprise Edition Omnibus install, GitLab.com uses
+the following applications and settings to achieve scale. All settings are
+located publicly available [chef cookbooks](https://gitlab.com/gitlab-cookbooks).
+
+### ELK
+
+We use Elasticsearch, logstash, and Kibana for part of our monitoring solution:
+
+- [gitlab-cookbooks / gitlab-elk · GitLab](https://gitlab.com/gitlab-cookbooks/gitlab-elk)
+- [gitlab-cookbooks / gitlab_elasticsearch · GitLab](https://gitlab.com/gitlab-cookbooks/gitlab_elasticsearch)
+
+### Prometheus
+
+Prometheus complete our monitoring stack:
+
+- [gitlab-cookbooks / gitlab-prometheus · GitLab](https://gitlab.com/gitlab-cookbooks/gitlab-prometheus)
+
+### Grafana
+
+For the visualization of monitoring data:
+
+- [gitlab-cookbooks / gitlab-grafana · GitLab](https://gitlab.com/gitlab-cookbooks/gitlab-grafana)
+
+### Sentry
+
+Open source error tracking:
+
+- [gitlab-cookbooks / gitlab-sentry · GitLab](https://gitlab.com/gitlab-cookbooks/gitlab-sentry)
+
+### Consul
+
+Service discovery:
+
+- [gitlab-cookbooks / gitlab_consul · GitLab](https://gitlab.com/gitlab-cookbooks/gitlab_consul)
+
+### Haproxy
+
+High Performance TCP/HTTP Load Balancer:
+
+- [gitlab-cookbooks / gitlab-haproxy · GitLab](https://gitlab.com/gitlab-cookbooks/gitlab-haproxy)
+
+[autoscale mode]: https://docs.gitlab.com/runner/configuration/autoscale.html "How Autoscale works"
+[runners-post]: https://about.gitlab.com/2016/04/05/shared-runners/ "Shared Runners on GitLab.com"
+[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-runner
+[altssh]: https://about.gitlab.com/2016/02/18/gitlab-dot-com-now-supports-an-alternate-git-plus-ssh-port/ "GitLab.com now supports an alternate git+ssh port"
+[GitLab Pages]: https://about.gitlab.com/features/pages "GitLab Pages"
+[docker in docker]: https://hub.docker.com/_/docker/ "Docker in Docker at DockerHub"
+[mailgun]: https://www.mailgun.com/ "Mailgun website"
+[sidekiq]: http://sidekiq.org/ "Sidekiq website"
+[unicorn-worker-killer]: https://rubygems.org/gems/unicorn-worker-killer "unicorn-worker-killer"
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index 022d6317555..930e506802a 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -41,7 +41,7 @@ select few, the amount of activity on the default Dashboard page can be
overwhelming. Changing this setting allows you to redefine what your default
dashboard will be.
-You have 6 options here that you can use for your default dashboard view:
+You have 8 options here that you can use for your default dashboard view:
- Your projects (default)
- Starred projects
@@ -49,6 +49,8 @@ You have 6 options here that you can use for your default dashboard view:
- Starred projects' activity
- Your groups
- Your [Todos]
+- Assigned Issues
+- Assigned Merge Requests
### Project home page content
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 661697aaeb7..716787532fc 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -71,7 +71,7 @@ You need Master [permissions] and above to access the Kubernetes page.
To add an existing Kubernetes cluster to your project:
1. Navigate to your project's **CI/CD > Kubernetes** page.
-1. Click on **Add Kuberntes cluster**.
+1. Click on **Add Kubernetes cluster**.
1. Click on **Add an existing Kubernetes cluster** and fill in the details:
- **Kubernetes cluster name** (required) - The name you wish to give the cluster.
- **Environment scope** (required)- The
@@ -101,7 +101,7 @@ To add an existing Kubernetes cluster to your project:
- If you or someone created a secret specifically for the project, usually
with limited permissions, the secret's namespace and project namespace may
be the same.
-1. Finally, click the **Create Kuberntes cluster** button.
+1. Finally, click the **Create Kubernetes cluster** button.
After a few moments, your cluster should be created. If something goes wrong,
you will be notified.
@@ -167,6 +167,17 @@ external IP address with the following procedure. It can be deployed using the
In order to publish your web application, you first need to find the external IP
address associated to your load balancer.
+### Let GitLab fetch the IP address
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17052) in GitLab 10.6.
+
+If you installed the Ingress [via the **Applications**](#installing-applications),
+you should see the Ingress IP address on this same page within a few minutes.
+If you don't see this, GitLab might not be able to determine the IP address of
+your ingress application in which case you should manually determine it.
+
+### Manually determining the IP address
+
If the cluster is on GKE, click on the **Google Kubernetes Engine** link in the
**Advanced settings**, or go directly to the
[Google Kubernetes Engine dashboard](https://console.cloud.google.com/kubernetes/)
@@ -193,6 +204,24 @@ The output is the external IP address of your cluster. This information can then
be used to set up DNS entries and forwarding rules that allow external access to
your deployed applications.
+### Using a static IP
+
+By default, an ephemeral external IP address is associated to the cluster's load
+balancer. If you associate the ephemeral IP with your DNS and the IP changes,
+your apps will not be able to be reached, and you'd have to change the DNS
+record again. In order to avoid that, you should change it into a static
+reserved IP.
+
+[Read how to promote an ephemeral external IP address in GKE.](https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address#promote_ephemeral_ip)
+
+### Pointing your DNS at the cluster IP
+
+Once you've set up the static IP, you should associate it to a [wildcard DNS
+record](https://en.wikipedia.org/wiki/Wildcard_DNS_record), in order to be able
+to reach your apps. This heavily depends on your domain provider, but in case
+you aren't sure, just create an A record with a wildcard host like
+`*.example.com.`.
+
## Setting the environment scope
NOTE: **Note:**
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 175a8975ae1..f94e93dd7d8 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -128,11 +128,9 @@ and Git push/pull redirects.
Depending on the situation, different things apply.
-When [renaming a user](../profile/index.md#changing-your-username) or
-[changing a group path](../group/index.md#changing-a-group-s-path):
+When [renaming a user](../profile/index.md#changing-your-username),
+[changing a group path](../group/index.md#changing-a-group-s-path) or [renaming a repository](settings/index.md#renaming-a-repository):
-- **The redirect to the new URL is permanent**, which means that the original
- namespace can't be claimed again by any group or user.
- Existing web URLs for the namespace and anything under it (e.g., projects) will
redirect to the new URLs.
- Starting with GitLab 10.3, existing Git remote URLs for projects under the
@@ -141,9 +139,5 @@ When [renaming a user](../profile/index.md#changing-your-username) or
your remote will be displayed instead of rejecting your action.
This means that any automation scripts, or Git clients will continue to
work after a rename, making any transition a lot smoother.
- To avoid pulling from or pushing to an entirely incorrect repository, the old
- path will be reserved.
-
-When [renaming-a-repository](settings/index.md#renaming-a-repository), the same
-things apply, except for the Git push/pull actions which will be rejected with a
-warning message to change to the new remote URL.
+- The redirects will be available as long as the original path is not claimed by
+ another group, user or project.
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 249463fb86e..fa7e504c4aa 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -2,7 +2,7 @@
> [Introduced][ce-8935] in GitLab 9.0.
-GitLab offers powerful integration with [Prometheus] for monitoring key metrics your apps, directly within GitLab.
+GitLab offers powerful integration with [Prometheus] for monitoring key metrics of your apps, directly within GitLab.
Metrics for each environment are retrieved from Prometheus, and then displayed
within the GitLab interface.
@@ -12,17 +12,21 @@ There are two ways to setup Prometheus integration, depending on where your apps
* For deployments on Kubernetes, GitLab can automatically [deploy and manage Prometheus](#managed-prometheus-on-kubernetes)
* For other deployment targets, simply [specify the Prometheus server](#manual-configuration-of-prometheus).
-## Managed Prometheus on Kubernetes
+Once enabled, GitLab will automatically detect metrics from known services in the [metric library](#monitoring-ci-cd-environments).
+
+## Enabling Prometheus Integration
+
+### Managed Prometheus on Kubernetes
> **Note**: [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28916) in GitLab 10.5
GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cluster](../clusters/index.md), making monitoring of your apps easy.
-### Requirements
+#### Requirements
* A [connected Kubernetes cluster](../clusters/index.md)
* Helm Tiller [installed by GitLab](../clusters/index.md#installing-applications)
-### Getting started
+#### Getting started
Once you have a connected Kubernetes cluster with Helm installed, deploying a managed Prometheus is as easy as a single click.
@@ -32,7 +36,7 @@ Once you have a connected Kubernetes cluster with Helm installed, deploying a ma
![Managed Prometheus Deploy](img/prometheus_deploy.png)
-### About managed Prometheus deployments
+#### About managed Prometheus deployments
Prometheus is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/prometheus). Prometheus is only accessible within the cluster, with GitLab communicating through the [Kubernetes API](https://kubernetes.io/docs/concepts/overview/kubernetes-api/).
@@ -45,9 +49,9 @@ CPU and Memory consumption is monitored, but requires [naming conventions](prome
The [NGINX Ingress](../clusters/index.md#installing-applications) that is deployed by GitLab to clusters, is automatically annotated for monitoring providing key response metrics: latency, throughput, and error rates.
-## Manual configuration of Prometheus
+### Manual configuration of Prometheus
-### Requirements
+#### Requirements
Integration with Prometheus requires the following:
@@ -56,7 +60,7 @@ Integration with Prometheus requires the following:
1. Each metric must be have a label to indicate the environment
1. GitLab must have network connectivity to the Prometheus server
-### Getting started
+#### Getting started
Installing and configuring Prometheus to monitor applications is fairly straight forward.
@@ -64,7 +68,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight
1. Set up one of the [supported monitoring targets](prometheus_library/metrics.md)
1. Configure the Prometheus server to [collect their metrics](https://prometheus.io/docs/operating/configuration/#scrape_config)
-### Configuration in GitLab
+#### Configuration in GitLab
The actual configuration of Prometheus integration within GitLab is very simple.
All you will need is the DNS or IP address of the Prometheus server you'd like
@@ -83,9 +87,9 @@ to integrate with.
Once configured, GitLab will attempt to retrieve performance metrics for any
environment which has had a successful deployment.
-GitLab will automatically scan the Prometheus server for known metrics and attempt to identify the metrics for a particular environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/metrics.html).
+GitLab will automatically scan the Prometheus server for metrics from known serves like Kubernetes and NGINX, and attempt to identify individual environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/metrics.html).
-[Learn more about monitoring environments.](../../../ci/environments.md#monitoring-environments)
+You can view the performance dashboard for an environment by [clicking on the monitoring button](../../../ci/environments.md#monitoring-environments).
## Determining the performance impact of a merge
@@ -93,7 +97,7 @@ GitLab will automatically scan the Prometheus server for known metrics and attem
> GitLab 9.3 added the [numeric comparison](https://gitlab.com/gitlab-org/gitlab-ce/issues/27439) of the 30 minute averages.
> Requires [Kubernetes](prometheus_library/kubernetes.md) metrics
-Developers can view theperformance impact of their changes within the merge
+Developers can view the performance impact of their changes within the merge
request workflow. When a source branch has been deployed to an environment, a sparkline and numeric comparison of the average memory consumption will appear. On the sparkline, a dot
indicates when the current changes were deployed, with up to 30 minutes of
performance data displayed before and after. The comparison shows the difference between the 30 minute average before and after the deployment. This information is updated after
@@ -109,7 +113,7 @@ Prometheus server.
## Troubleshooting
-If the "Attempting to load performance data" screen continues to appear, it could be due to:
+If the "No data found" screen continues to appear, it could be due to:
- No successful deployments have occurred to this environment.
- Prometheus does not have performance data for this environment, or the metrics
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index 8ac753c07bf..6b190deaa6c 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -11,10 +11,17 @@ integration services must be enabled.
## Metrics supported
-| Name | Query |
-| ---- | ----- |
-| Average Memory Usage (MB) | avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024 |
-| Average CPU Utilization (%) | avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name)) |
+- Average Memory Usage (MB):
+
+ ```
+ avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024
+ ```
+
+- Average CPU Utilization (%):
+
+ ```
+ avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))
+ ```
## Configuring Prometheus to monitor for Kubernetes metrics
diff --git a/doc/user/project/merge_requests/img/remove_source_branch_status.png b/doc/user/project/merge_requests/img/remove_source_branch_status.png
new file mode 100644
index 00000000000..1377fab54ec
--- /dev/null
+++ b/doc/user/project/merge_requests/img/remove_source_branch_status.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 10d67729734..3640d236db4 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -77,6 +77,22 @@ You can [search and filter the results](../../search/index.md#issues-and-merge-r
![Group Issues list view](img/group_merge_requests_list_view.png)
+## Removing the source branch
+
+When creating a merge request, select the "Remove source branch when merge
+request accepted" option and the source branch will be removed when the merge
+request is merged.
+
+This option is also visible in an existing merge request next to the merge
+request button and can be selected/deselected before merging. It's only visible
+to users with [Master permissions](../../permissions.md) in the source project.
+
+If the user viewing the merge request does not have the correct permissions to
+remove the source branch and the source branch is set for removal, the merge
+request widget will show the "Removes source branch" text.
+
+![Remove source branch status](img/remove_source_branch_status.png)
+
## Authorization for merge requests
There are two main ways to have a merge request flow with GitLab:
diff --git a/doc/user/project/merge_requests/maintainer_access.md b/doc/user/project/merge_requests/maintainer_access.md
index 7feccc28f6b..c9763a3fe02 100644
--- a/doc/user/project/merge_requests/maintainer_access.md
+++ b/doc/user/project/merge_requests/maintainer_access.md
@@ -1,12 +1,17 @@
# Allow maintainer pushes for merge requests across forks
+> [Introduced][ce-17395] in GitLab 10.6.
+
This feature is available for merge requests across forked projects that are
-publicly accessible. It makes it easier for maintainers of projects to collaborate
-on merge requests across forks.
+publicly accessible. It makes it easier for maintainers of projects to
+collaborate on merge requests across forks.
-When enabling this feature for a merge request, you give can give members with push access to the target project rights to edit files on the source branch of the merge request.
+When enabled for a merge request, members with merge access to the target
+branch of the project will be granted write permissions to the source branch
+of the merge request.
-The feature can only be enabled by users who already have push access to the source project. And only lasts while the merge request is open.
+The feature can only be enabled by users who already have push access to the
+source project, and only lasts while the merge request is open.
Enable this functionality while creating a merge request:
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index 34809a2826f..a13b1b4561c 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -12,7 +12,7 @@ month on the 22nd for a certain branch.
In order to schedule a pipeline:
-1. Navigate to your project's **Pipelines âž” Schedules** and click the
+1. Navigate to your project's **CI / CD âž” Schedules** and click the
**New Schedule** button.
1. Fill in the form
1. Hit **Save pipeline schedule** for the changes to take effect.
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index d768b73286d..cac3cb599dd 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -5,6 +5,7 @@ Documentation on how to use Git LFS are under [Managing large binary files with
## Requirements
* Git LFS is supported in GitLab starting with version 8.2.
+* Support for object storage, such as AWS S3, was introduced in 10.0.
* Users need to install [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up.
## Configuration
@@ -12,16 +13,18 @@ Documentation on how to use Git LFS are under [Managing large binary files with
Git LFS objects can be large in size. By default, they are stored on the server
GitLab is installed on.
-There are two configuration options to help GitLab server administrators:
+There are various configuration options to help GitLab server administrators:
* Enabling/disabling Git LFS support
* Changing the location of LFS object storage
+* Setting up AWS S3 compatible object storage
### Omnibus packages
In `/etc/gitlab/gitlab.rb`:
```ruby
+# Change to true to enable lfs
gitlab_rails['lfs_enabled'] = false
# Optionally, change the storage path location. Defaults to
@@ -35,11 +38,115 @@ gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
In `config/gitlab.yml`:
```yaml
+# Change to true to enable lfs
lfs:
enabled: false
storage_path: /mnt/storage/lfs-objects
```
+## Setting up S3 compatible object storage
+
+> **Note:** [Introduced][ee-2760] in [GitLab Premium][eep] 10.0.
+> Available in [GitLab CE][ce] 10.7
+
+It is possible to store LFS objects on remote object storage instead of on a local disk.
+
+This allows you to offload storage to an external AWS S3 compatible service, freeing up disk space locally. You can also host your own S3 compatible storage decoupled from GitLab, with with a service such as [Minio](https://www.minio.io/).
+
+Object storage currently transfers files first to GitLab, and then on the object storage in a second stage. This can be done either by using a rake task to transfer existing objects, or in a background job after each file is received.
+
+### Object Storage Settings
+
+For source installations the following settings are nested under `lfs:` and then `object_store:`. On omnibus installs they are prefixed by `lfs_object_store_`.
+
+| Setting | Description | Default |
+|---------|-------------|---------|
+| `enabled` | Enable/disable object storage | `false` |
+| `remote_directory` | The bucket name where LFS objects will be stored| |
+| `direct_upload` | Set to true to enable direct upload of LFS without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. | `false` |
+| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` |
+| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
+| `connection` | Various connection options described below | |
+
+#### S3 compatible connection settings
+
+The connection settings match those provided by [Fog](https://github.com/fog), and are as follows:
+
+| Setting | Description | Default |
+|---------|-------------|---------|
+| `provider` | Always `AWS` for compatible hosts | AWS |
+| `aws_access_key_id` | AWS credentials, or compatible | |
+| `aws_secret_access_key` | AWS credentials, or compatible | |
+| `region` | AWS region | us-east-1 |
+| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
+| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
+| `path_style` | Set to true to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as false for AWS S3 | false |
+
+
+### From source
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
+ lines:
+
+ ```yaml
+ lfs:
+ enabled: true
+ object_store:
+ enabled: false
+ remote_directory: lfs-objects # Bucket name
+ connection:
+ provider: AWS
+ aws_access_key_id: 1ABCD2EFGHI34JKLM567N
+ aws_secret_access_key: abcdefhijklmnopQRSTUVwxyz0123456789ABCDE
+ region: eu-central-1
+ # Use the following options to configure an AWS compatible host such as Minio
+ host: 'localhost'
+ endpoint: 'http://127.0.0.1:9000'
+ path_style: true
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Migrate any existing local LFS objects to the object storage:
+
+ ```bash
+ sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production
+ ```
+
+ This will migrate existing LFS objects to object storage. New LFS objects
+ will be forwarded to object storage unless
+ `gitlab_rails['lfs_object_store_background_upload']` is set to false.
+
+### In Omnibus
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
+ the values you want:
+
+ ```ruby
+ gitlab_rails['lfs_object_store_enabled'] = true
+ gitlab_rails['lfs_object_store_remote_directory'] = "lfs-objects"
+ gitlab_rails['lfs_object_store_connection'] = {
+ 'provider' => 'AWS',
+ 'region' => 'eu-central-1',
+ 'aws_access_key_id' => '1ABCD2EFGHI34JKLM567N',
+ 'aws_secret_access_key' => 'abcdefhijklmnopQRSTUVwxyz0123456789ABCDE',
+ # The below options configure an S3 compatible host instead of AWS
+ 'host' => 'localhost',
+ 'endpoint' => 'http://127.0.0.1:9000',
+ 'path_style' => true
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab]s for the changes to take effect.
+1. Migrate any existing local LFS objects to the object storage:
+
+ ```bash
+ gitlab-rake gitlab:lfs:migrate
+ ```
+
+ This will migrate existing LFS objects to object storage. New LFS objects
+ will be forwarded to object storage unless
+ `gitlab_rails['lfs_object_store_background_upload']` is set to false.
+
## Storage statistics
You can see the total storage used for LFS objects on groups and projects
@@ -48,10 +155,13 @@ and [projects APIs](../../api/projects.md).
## Known limitations
-* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
- is not supported
* Support for removing unreferenced LFS objects was added in 8.14 onwards.
* LFS authentications via SSH was added with GitLab 8.12
* Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2.
* The storage statistics currently count each LFS object multiple times for
every project linking to it
+
+[reconfigure gitlab]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
+[restart gitlab]: ../../administration/restart_gitlab.md#installations-from-source "How to restart GitLab"
+[eep]: https://about.gitlab.com/products/ "GitLab Premium"
+[ee-2760]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2760
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 37265a5b771..c4095ee0f69 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -67,7 +67,7 @@ Below is the table of events users can be notified of:
### Issue / Merge request events
-In all of the below cases, the notification will be sent to:
+In most of the below cases, the notification will be sent to:
- Participants:
- the author and assignee of the issue/merge request
- authors of comments on the issue/merge request
@@ -87,6 +87,7 @@ In all of the below cases, the notification will be sent to:
| Reassign issue | The above, plus the old assignee |
| Reopen issue | |
| New merge request | |
+| Push to merge request | Participants and Custom notification level with this event selected |
| Reassign merge request | The above, plus the old assignee |
| Close merge request | |
| Reopen merge request | |
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 982f45425a3..684955a1b24 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -231,6 +231,20 @@ module API
render_api_error!("Failed to save note #{note.errors.messages}", 400)
end
end
+
+ desc 'Get Merge Requests associated with a commit' do
+ success Entities::MergeRequestBasic
+ end
+ params do
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag on which to find Merge Requests'
+ use :pagination
+ end
+ get ':id/repository/commits/:sha/merge_requests', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
+ commit = user_project.commit(params[:sha])
+ not_found! 'Commit' unless commit
+
+ present paginate(commit.merge_requests), with: Entities::MergeRequestBasic
+ end
end
end
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index e4fca77ab5d..e59e8a45908 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -410,7 +410,7 @@ module API
)
end
- def present_file!(path, filename, content_type = 'application/octet-stream')
+ def present_disk_file!(path, filename, content_type = 'application/octet-stream')
filename ||= File.basename(path)
header['Content-Disposition'] = "attachment; filename=#{filename}"
header['Content-Transfer-Encoding'] = 'binary'
@@ -426,13 +426,17 @@ module API
end
end
- def present_artifacts!(artifacts_file)
- return not_found! unless artifacts_file.exists?
+ def present_carrierwave_file!(file, supports_direct_download: true)
+ return not_found! unless file.exists?
- if artifacts_file.file_storage?
- present_file!(artifacts_file.path, artifacts_file.filename)
+ if file.file_storage?
+ present_disk_file!(file.path, file.filename)
+ elsif supports_direct_download && file.class.direct_download_enabled?
+ redirect(file.url)
else
- redirect_to(artifacts_file.url)
+ header(*Gitlab::Workhorse.send_url(file.url))
+ status :ok
+ body
end
end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 4b564cfdef2..14648588dfd 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -109,7 +109,7 @@ module API
# Return the Gitaly Address if it is enabled
def gitaly_payload(action)
- return unless %w[git-receive-pack git-upload-pack].include?(action)
+ return unless %w[git-receive-pack git-upload-pack git-upload-archive].include?(action)
{
repository: repository.gitaly_repository,
diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb
index 47e5eeab31d..b1adef49d46 100644
--- a/lib/api/job_artifacts.rb
+++ b/lib/api/job_artifacts.rb
@@ -28,7 +28,7 @@ module API
builds = user_project.latest_successful_builds_for(params[:ref_name])
latest_build = builds.find_by!(name: params[:job])
- present_artifacts!(latest_build.artifacts_file)
+ present_carrierwave_file!(latest_build.artifacts_file)
end
desc 'Download the artifacts archive from a job' do
@@ -43,7 +43,7 @@ module API
build = find_build!(params[:job_id])
- present_artifacts!(build.artifacts_file)
+ present_carrierwave_file!(build.artifacts_file)
end
desc 'Download a specific file from artifacts archive' do
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 9c205514b3a..60911c8d733 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -72,7 +72,7 @@ module API
present build, with: Entities::Job
end
- # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+ # TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace
# is saved in the DB instead of file). But before that, we need to consider how to replace the value of
# `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
desc 'Get a trace of a specific job of a project'
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index b0a7fd6f4ab..efc4a33ae1b 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -25,7 +25,7 @@ module API
render_api_error!('404 Not found or has expired', 404) unless path
- present_file!(path, File.basename(path), 'application/gzip')
+ present_disk_file!(path, File.basename(path), 'application/gzip')
end
desc 'Start export' do
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index b552b0e0c5d..467bc78dad8 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -228,11 +228,7 @@ module API
namespace_id = fork_params[:namespace]
if namespace_id.present?
- fork_params[:namespace] = if namespace_id =~ /^\d+$/
- Namespace.find_by(id: namespace_id)
- else
- Namespace.find_by_path_or_name(namespace_id)
- end
+ fork_params[:namespace] = find_namespace(namespace_id)
unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
not_found!('Target Namespace')
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index c15c487deb4..aa7cab4a741 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -52,11 +52,7 @@ module API
conflict!("Protected branch '#{params[:name]}' already exists")
end
- # Replace with `declared(params)` after updating to grape v1.0.2
- # See https://github.com/ruby-grape/grape/pull/1710
- # and https://gitlab.com/gitlab-org/gitlab-ce/issues/40843
- declared_params = params.slice("name", "push_access_level", "merge_access_level", "allowed_to_push", "allowed_to_merge")
-
+ declared_params = declared_params(include_missing: false)
api_service = ::ProtectedBranches::ApiService.new(user_project, current_user, declared_params)
protected_branch = api_service.create
@@ -74,7 +70,10 @@ module API
delete ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
protected_branch = user_project.protected_branches.find_by!(name: params[:name])
- destroy_conditionally!(protected_branch)
+ destroy_conditionally!(protected_branch) do
+ destroy_service = ::ProtectedBranches::DestroyService.new(user_project, current_user)
+ destroy_service.execute(protected_branch)
+ end
end
end
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 7e6c33ec33d..8da97a97754 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -244,11 +244,12 @@ module API
params do
requires :id, type: Integer, desc: %q(Job's ID)
optional :token, type: String, desc: %q(Job's authentication token)
+ optional :direct_download, default: false, type: Boolean, desc: %q(Perform direct download from remote storage instead of proxying artifacts)
end
get '/:id/artifacts' do
job = authenticate_job!
- present_artifacts!(job.artifacts_file)
+ present_carrierwave_file!(job.artifacts_file, supports_direct_download: params[:direct_download])
end
end
end
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 3556ad98c52..5d9ec617cb7 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -84,7 +84,7 @@ module API
values: %w(projects issues merge_requests milestones)
use :pagination
end
- get ':id/-/search' do
+ get ':id/(-/)search' do
present search(group_id: user_group.id), with: entity
end
end
@@ -103,7 +103,7 @@ module API
values: %w(issues merge_requests milestones notes wiki_blobs commits blobs)
use :pagination
end
- get ':id/-/search' do
+ get ':id/(-/)search' do
present search(project_id: user_project.id), with: entity
end
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 6c97659166d..794fdab8f2b 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -735,7 +735,7 @@ module API
required: false,
name: event_name.to_sym,
type: String,
- desc: ServicesHelper.service_event_description(event_name)
+ desc: service.event_description(event_name)
}
end
end
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
index ac76fece931..683b9c993cb 100644
--- a/lib/api/v3/builds.rb
+++ b/lib/api/v3/builds.rb
@@ -85,7 +85,7 @@ module API
build = get_build!(params[:build_id])
- present_artifacts!(build.artifacts_file)
+ present_carrierwave_file!(build.artifacts_file)
end
desc 'Download the artifacts file from build' do
@@ -102,10 +102,10 @@ module API
builds = user_project.latest_successful_builds_for(params[:ref_name])
latest_build = builds.find_by!(name: params[:job])
- present_artifacts!(latest_build.artifacts_file)
+ present_carrierwave_file!(latest_build.artifacts_file)
end
- # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+ # TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace
# is saved in the DB instead of file). But before that, we need to consider how to replace the value of
# `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
desc 'Get a trace of a specific build of a project'
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index 7d8b1f369fe..a2df969d819 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -268,11 +268,7 @@ module API
namespace_id = fork_params[:namespace]
if namespace_id.present?
- fork_params[:namespace] = if namespace_id =~ /^\d+$/
- Namespace.find_by(id: namespace_id)
- else
- Namespace.find_by_path_or_name(namespace_id)
- end
+ fork_params[:namespace] = find_namespace(namespace_id)
unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace])
not_found!('Target Namespace')
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 6715159a1aa..88a7f2a4235 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -65,7 +65,7 @@ module Backup
def restore
Gitlab.config.repositories.storages.each do |name, repository_storage|
- path = repository_storage['path']
+ path = repository_storage.legacy_disk_path
next unless File.exist?(path)
# Move repos dir to 'repositories.old' dir
@@ -200,7 +200,7 @@ module Backup
end
def repository_storage_paths_args
- Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
+ Gitlab.config.repositories.storages.values.map { |rs| rs.legacy_disk_path }
end
def progress
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index 75b64ae9af2..4a143baeef6 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -21,12 +21,13 @@ module Banzai
#
# See http://en.wikipedia.org/wiki/URI_scheme
#
- # The negative lookbehind ensures that users can paste a URL followed by a
- # period or comma for punctuation without those characters being included
- # in the generated link.
+ # The negative lookbehind ensures that users can paste a URL followed by
+ # punctuation without those characters being included in the generated
+ # link. It matches the behaviour of Rinku 2.0.1:
+ # https://github.com/vmg/rinku/blob/v2.0.1/ext/rinku/autolink.c#L65
#
- # Rubular: http://rubular.com/r/JzPhi6DCZp
- LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://[^\s>]+)(?<!,|\.)}
+ # Rubular: http://rubular.com/r/nrL3r9yUiq
+ LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://[^\s>]+)(?<!\?|!|\.|,|:)}
# Text matching LINK_PATTERN inside these elements will not be linked
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
@@ -104,8 +105,12 @@ module Banzai
end
end
- options = link_options.merge(href: match)
- content_tag(:a, match.html_safe, options) + dropped
+ # match has come from node.to_html above, so we know it's encoded
+ # correctly.
+ html_safe_match = match.html_safe
+ options = link_options.merge(href: html_safe_match)
+
+ content_tag(:a, html_safe_match, options) + dropped
end
def autolink_filter(text)
diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb
index 77299abe324..8f541dcfdb2 100644
--- a/lib/banzai/filter/issuable_state_filter.rb
+++ b/lib/banzai/filter/issuable_state_filter.rb
@@ -17,7 +17,7 @@ module Banzai
issuables.each do |node, issuable|
next if !can_read_cross_project? && issuable.project != project
- if VISIBLE_STATES.include?(issuable.state) && node.inner_html == issuable.reference_link_text(project)
+ if VISIBLE_STATES.include?(issuable.state) && issuable_reference?(node.inner_html, issuable)
node.content += " (#{issuable.state})"
end
end
@@ -27,6 +27,10 @@ module Banzai
private
+ def issuable_reference?(text, issuable)
+ text == issuable.reference_link_text(project || group)
+ end
+
def can_read_cross_project?
Ability.allowed?(current_user, :read_cross_project)
end
@@ -38,6 +42,10 @@ module Banzai
def project
context[:project]
end
+
+ def group
+ context[:group]
+ end
end
end
end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 4001b8a85e3..8b2f05fffec 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -2,10 +2,10 @@ module Banzai
module Pipeline
class GfmPipeline < BasePipeline
# These filters convert GitLab Flavored Markdown (GFM) to HTML.
- # The handlers defined in app/assets/javascripts/copy_as_gfm.js
+ # The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
# consequently convert that same HTML to GFM to be copied to the clipboard.
# Every filter that generates HTML from GFM should have a handler in
- # app/assets/javascripts/copy_as_gfm.js, in reverse order.
+ # app/assets/javascripts/behaviors/markdown/copy_as_gfm.js, in reverse order.
# The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
def self.filters
@filters ||= FilterArray[
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index f5ccf952cf9..6af763faf10 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -69,7 +69,11 @@ module Gitlab
authenticators.compact!
- user if authenticators.find { |auth| auth.login(login, password) }
+ # return found user that was authenticated first for given login credentials
+ authenticators.find do |auth|
+ authenticated_user = auth.login(login, password)
+ break authenticated_user if authenticated_user
+ end
end
end
diff --git a/lib/gitlab/auth/database/authentication.rb b/lib/gitlab/auth/database/authentication.rb
index 260a77058a4..1234ace0334 100644
--- a/lib/gitlab/auth/database/authentication.rb
+++ b/lib/gitlab/auth/database/authentication.rb
@@ -8,7 +8,7 @@ module Gitlab
def login(login, password)
return false unless Gitlab::CurrentSettings.password_authentication_enabled_for_git?
- user&.valid_password?(password)
+ return user if user&.valid_password?(password)
end
end
end
diff --git a/lib/gitlab/auth/ldap/authentication.rb b/lib/gitlab/auth/ldap/authentication.rb
index e70c3ab6b46..7c134fb6438 100644
--- a/lib/gitlab/auth/ldap/authentication.rb
+++ b/lib/gitlab/auth/ldap/authentication.rb
@@ -12,30 +12,26 @@ module Gitlab
return unless Gitlab::Auth::LDAP::Config.enabled?
return unless login.present? && password.present?
- auth = nil
- # loop through providers until valid bind
+ # return found user that was authenticated by first provider for given login credentials
providers.find do |provider|
auth = new(provider)
- auth.login(login, password) # true will exit the loop
+ break auth.user if auth.login(login, password) # true will exit the loop
end
-
- # If (login, password) was invalid for all providers, the value of auth is now the last
- # Gitlab::Auth::LDAP::Authentication instance we tried.
- auth.user
end
def self.providers
Gitlab::Auth::LDAP::Config.providers
end
- attr_accessor :ldap_user
-
def login(login, password)
- @ldap_user = adapter.bind_as(
+ result = adapter.bind_as(
filter: user_filter(login),
size: 1,
password: password
)
+ return unless result
+
+ @user = Gitlab::Auth::LDAP::User.find_by_uid_and_provider(result.dn, provider)
end
def adapter
@@ -56,12 +52,6 @@ module Gitlab
filter
end
-
- def user
- return unless ldap_user
-
- Gitlab::Auth::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
- end
end
end
end
diff --git a/lib/gitlab/auth/o_auth/authentication.rb b/lib/gitlab/auth/o_auth/authentication.rb
index ed03b9f8b40..d4e7f35c857 100644
--- a/lib/gitlab/auth/o_auth/authentication.rb
+++ b/lib/gitlab/auth/o_auth/authentication.rb
@@ -12,6 +12,7 @@ module Gitlab
@user = user
end
+ # Implementation must return user object if login successful
def login(login, password)
raise NotImplementedError
end
diff --git a/lib/gitlab/bare_repository_import/importer.rb b/lib/gitlab/bare_repository_import/importer.rb
index 884a3de8f62..1a25138e7d6 100644
--- a/lib/gitlab/bare_repository_import/importer.rb
+++ b/lib/gitlab/bare_repository_import/importer.rb
@@ -63,7 +63,7 @@ module Gitlab
log " * Created #{project.name} (#{project_full_path})".color(:green)
project.write_repository_config
- project.repository.create_hooks
+ Gitlab::Git::Repository.create_hooks(project.repository.path_to_repo, Gitlab.config.gitlab_shell.hooks_path)
ProjectCacheWorker.perform_async(project.id)
else
diff --git a/lib/gitlab/checks/project_moved.rb b/lib/gitlab/checks/project_moved.rb
index 3263790a876..3a197078d08 100644
--- a/lib/gitlab/checks/project_moved.rb
+++ b/lib/gitlab/checks/project_moved.rb
@@ -9,20 +9,16 @@ module Gitlab
super(project, user, protocol)
end
- def message(rejected: false)
+ def message
<<~MESSAGE
Project '#{redirected_path}' was moved to '#{project.full_path}'.
Please update your Git remote:
- #{remote_url_message(rejected)}
+ git remote set-url origin #{url_to_repo}
MESSAGE
end
- def permanent_redirect?
- RedirectRoute.permanent.exists?(path: redirected_path)
- end
-
private
attr_reader :redirected_path
@@ -30,18 +26,6 @@ module Gitlab
def self.message_key(user_id, project_id)
"#{REDIRECT_NAMESPACE}:#{user_id}:#{project_id}"
end
-
- def remote_url_message(rejected)
- if rejected
- "git remote set-url origin #{url_to_repo} and try again."
- else
- "git remote set-url origin #{url_to_repo}"
- end
- end
-
- def url
- protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
- end
end
end
end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index f7ff7ea212e..66ac4a40616 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -4,7 +4,8 @@ module Gitlab
# Base GitLab CI Configuration facade
#
class Config
- def initialize(config)
+ # EE would override this and utilize opts argument
+ def initialize(config, opts = {})
@config = Loader.new(config).load!
@global = Entry::Global.new(@config)
diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb
index d5e17a123df..f4c8d5342c1 100644
--- a/lib/gitlab/ci/pipeline/chain/create.rb
+++ b/lib/gitlab/ci/pipeline/chain/create.rb
@@ -9,11 +9,16 @@ module Gitlab
::Ci::Pipeline.transaction do
pipeline.save!
- @command.seeds_block&.call(pipeline)
-
- ::Ci::CreatePipelineStagesService
- .new(project, current_user)
- .execute(pipeline)
+ ##
+ # Create environments before the pipeline starts.
+ #
+ pipeline.builds.each do |build|
+ if build.has_environment?
+ project.environments.find_or_create_by(
+ name: build.expanded_environment_name
+ )
+ end
+ end
end
rescue ActiveRecord::RecordInvalid => e
error("Failed to persist the pipeline: #{e}")
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
new file mode 100644
index 00000000000..b2b00c8cb4b
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class Populate < Chain::Base
+ include Chain::Helpers
+
+ PopulateError = Class.new(StandardError)
+
+ def perform!
+ ##
+ # Populate pipeline with block argument of CreatePipelineService#execute.
+ #
+ @command.seeds_block&.call(pipeline)
+
+ ##
+ # Populate pipeline with all stages and builds from pipeline seeds.
+ #
+ pipeline.stage_seeds.each do |stage|
+ stage.user = current_user
+
+ pipeline.stages << stage.to_resource
+
+ stage.seeds.each do |build|
+ pipeline.builds << build.to_resource
+ end
+ end
+
+ if pipeline.stages.none?
+ return error('No stages / jobs for this pipeline.')
+ end
+
+ if pipeline.invalid?
+ return error('Failed to build the pipeline!')
+ end
+
+ raise Populate::PopulateError if pipeline.persisted?
+ end
+
+ def break?
+ pipeline.errors.any?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/config.rb b/lib/gitlab/ci/pipeline/chain/validate/config.rb
index 075504bcce5..a3bd2a5a23a 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/config.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/config.rb
@@ -16,11 +16,7 @@ module Gitlab
@pipeline.drop!(:config_error)
end
- return error(@pipeline.yaml_errors)
- end
-
- unless @pipeline.has_stage_seeds?
- return error('No stages / jobs for this pipeline.')
+ error(@pipeline.yaml_errors)
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/base.rb b/lib/gitlab/ci/pipeline/seed/base.rb
new file mode 100644
index 00000000000..db9706924bb
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/seed/base.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Seed
+ class Base
+ def attributes
+ raise NotImplementedError
+ end
+
+ def included?
+ raise NotImplementedError
+ end
+
+ def to_resource
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
new file mode 100644
index 00000000000..7cd7c864448
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Seed
+ class Build < Seed::Base
+ include Gitlab::Utils::StrongMemoize
+
+ delegate :dig, to: :@attributes
+
+ def initialize(pipeline, attributes)
+ @pipeline = pipeline
+ @attributes = attributes
+
+ @only = attributes.delete(:only)
+ @except = attributes.delete(:except)
+ end
+
+ def user=(current_user)
+ @attributes.merge!(user: current_user)
+ end
+
+ def included?
+ strong_memoize(:inclusion) do
+ only_specs = Gitlab::Ci::Build::Policy.fabricate(@only)
+ except_specs = Gitlab::Ci::Build::Policy.fabricate(@except)
+
+ only_specs.all? { |spec| spec.satisfied_by?(@pipeline) } &&
+ except_specs.none? { |spec| spec.satisfied_by?(@pipeline) }
+ end
+ end
+
+ def attributes
+ @attributes.merge(
+ pipeline: @pipeline,
+ project: @pipeline.project,
+ ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: @pipeline.legacy_trigger,
+ protected: @pipeline.protected_ref?
+ )
+ end
+
+ def to_resource
+ strong_memoize(:resource) do
+ ::Ci::Build.new(attributes)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
new file mode 100644
index 00000000000..1fcbdc1b15a
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -0,0 +1,51 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Seed
+ class Stage < Seed::Base
+ include Gitlab::Utils::StrongMemoize
+
+ delegate :size, to: :seeds
+ delegate :dig, to: :seeds
+
+ def initialize(pipeline, attributes)
+ @pipeline = pipeline
+ @attributes = attributes
+
+ @builds = attributes.fetch(:builds).map do |attributes|
+ Seed::Build.new(@pipeline, attributes)
+ end
+ end
+
+ def user=(current_user)
+ @builds.each { |seed| seed.user = current_user }
+ end
+
+ def attributes
+ { name: @attributes.fetch(:name),
+ pipeline: @pipeline,
+ project: @pipeline.project }
+ end
+
+ def seeds
+ strong_memoize(:seeds) do
+ @builds.select(&:included?)
+ end
+ end
+
+ def included?
+ seeds.any?
+ end
+
+ def to_resource
+ strong_memoize(:stage) do
+ ::Ci::Stage.new(attributes).tap do |stage|
+ seeds.each { |seed| stage.builds << seed.to_resource }
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/stage/seed.rb b/lib/gitlab/ci/stage/seed.rb
deleted file mode 100644
index f33c87f554d..00000000000
--- a/lib/gitlab/ci/stage/seed.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-module Gitlab
- module Ci
- module Stage
- class Seed
- include ::Gitlab::Utils::StrongMemoize
-
- attr_reader :pipeline
-
- delegate :project, to: :pipeline
- delegate :size, to: :@jobs
-
- def initialize(pipeline, stage, jobs)
- @pipeline = pipeline
- @stage = { name: stage }
- @jobs = jobs.to_a.dup
- end
-
- def user=(current_user)
- @jobs.map! do |attributes|
- attributes.merge(user: current_user)
- end
- end
-
- def stage
- @stage.merge(project: project)
- end
-
- def builds
- trigger = pipeline.trigger_requests.first
-
- @jobs.map do |attributes|
- attributes.merge(project: project,
- ref: pipeline.ref,
- tag: pipeline.tag,
- trigger_request: trigger,
- protected: protected_ref?)
- end
- end
-
- def create!
- pipeline.stages.create!(stage).tap do |stage|
- builds_attributes = builds.map do |attributes|
- attributes.merge(stage_id: stage.id)
- end
-
- pipeline.builds.create!(builds_attributes).each do |build|
- yield build if block_given?
- end
- end
- end
-
- private
-
- def protected_ref?
- strong_memoize(:protected_ref) do
- project.protected_for?(pipeline.ref)
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/trace/http_io.rb b/lib/gitlab/ci/trace/http_io.rb
new file mode 100644
index 00000000000..ac4308f4e2c
--- /dev/null
+++ b/lib/gitlab/ci/trace/http_io.rb
@@ -0,0 +1,187 @@
+##
+# This class is compatible with IO class (https://ruby-doc.org/core-2.3.1/IO.html)
+# source: https://gitlab.com/snippets/1685610
+module Gitlab
+ module Ci
+ class Trace
+ class HttpIO
+ BUFFER_SIZE = 128.kilobytes
+
+ InvalidURLError = Class.new(StandardError)
+ FailedToGetChunkError = Class.new(StandardError)
+
+ attr_reader :uri, :size
+ attr_reader :tell
+ attr_reader :chunk, :chunk_range
+
+ alias_method :pos, :tell
+
+ def initialize(url, size)
+ raise InvalidURLError unless ::Gitlab::UrlSanitizer.valid?(url)
+
+ @uri = URI(url)
+ @size = size
+ @tell = 0
+ end
+
+ def close
+ # no-op
+ end
+
+ def binmode
+ # no-op
+ end
+
+ def binmode?
+ true
+ end
+
+ def path
+ nil
+ end
+
+ def url
+ @uri.to_s
+ end
+
+ def seek(pos, where = IO::SEEK_SET)
+ new_pos =
+ case where
+ when IO::SEEK_END
+ size + pos
+ when IO::SEEK_SET
+ pos
+ when IO::SEEK_CUR
+ tell + pos
+ else
+ -1
+ end
+
+ raise 'new position is outside of file' if new_pos < 0 || new_pos > size
+
+ @tell = new_pos
+ end
+
+ def eof?
+ tell == size
+ end
+
+ def each_line
+ until eof?
+ line = readline
+ break if line.nil?
+
+ yield(line)
+ end
+ end
+
+ def read(length = nil)
+ out = ""
+
+ until eof? || (length && out.length >= length)
+ data = get_chunk
+ break if data.empty?
+
+ out << data
+ @tell += data.bytesize
+ end
+
+ out = out[0, length] if length && out.length > length
+
+ out
+ end
+
+ def readline
+ out = ""
+
+ until eof?
+ data = get_chunk
+ new_line = data.index("\n")
+
+ if !new_line.nil?
+ out << data[0..new_line]
+ @tell += new_line + 1
+ break
+ else
+ out << data
+ @tell += data.bytesize
+ end
+ end
+
+ out
+ end
+
+ def write(data)
+ raise NotImplementedError
+ end
+
+ def truncate(offset)
+ raise NotImplementedError
+ end
+
+ def flush
+ raise NotImplementedError
+ end
+
+ def present?
+ true
+ end
+
+ private
+
+ ##
+ # The below methods are not implemented in IO class
+ #
+ def in_range?
+ @chunk_range&.include?(tell)
+ end
+
+ def get_chunk
+ unless in_range?
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
+ http.request(request)
+ end
+
+ raise FailedToGetChunkError unless response.code == '200' || response.code == '206'
+
+ @chunk = response.body.force_encoding(Encoding::BINARY)
+ @chunk_range = response.content_range
+
+ ##
+ # Note: If provider does not return content_range, then we set it as we requested
+ # Provider: minio
+ # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # Provider: AWS
+ # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # Provider: GCS
+ # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
+ # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200
+ @chunk_range ||= (chunk_start...(chunk_start + @chunk.length))
+ end
+
+ @chunk[chunk_offset..BUFFER_SIZE]
+ end
+
+ def request
+ Net::HTTP::Get.new(uri).tap do |request|
+ request.set_range(chunk_start, BUFFER_SIZE)
+ end
+ end
+
+ def chunk_offset
+ tell % BUFFER_SIZE
+ end
+
+ def chunk_start
+ (tell / BUFFER_SIZE) * BUFFER_SIZE
+ end
+
+ def chunk_end
+ [chunk_start + BUFFER_SIZE, size].min
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index d52194f688b..b3fe3ef1c4d 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -8,7 +8,7 @@ module Gitlab
attr_reader :stream
- delegate :close, :tell, :seek, :size, :path, :truncate, to: :stream, allow_nil: true
+ delegate :close, :tell, :seek, :size, :path, :url, :truncate, to: :stream, allow_nil: true
delegate :valid?, to: :stream, as: :present?, allow_nil: true
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index a7285ac8f9d..e829f2a95f8 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -7,8 +7,8 @@ module Gitlab
attr_reader :cache, :stages, :jobs
- def initialize(config)
- @ci_config = Gitlab::Ci::Config.new(config)
+ def initialize(config, opts = {})
+ @ci_config = Gitlab::Ci::Config.new(config, opts)
@config = @ci_config.to_hash
unless @ci_config.valid?
@@ -27,7 +27,7 @@ module Gitlab
end
def build_attributes(name)
- job = @jobs[name.to_sym] || {}
+ job = @jobs.fetch(name.to_sym, {})
{ stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
@@ -53,37 +53,31 @@ module Gitlab
}.compact }
end
- def pipeline_stage_builds(stage, pipeline)
- selected_jobs = @jobs.select do |_, job|
- next unless job[:stage] == stage
-
- only_specs = Gitlab::Ci::Build::Policy
- .fabricate(job.fetch(:only, {}))
- except_specs = Gitlab::Ci::Build::Policy
- .fabricate(job.fetch(:except, {}))
-
- only_specs.all? { |spec| spec.satisfied_by?(pipeline) } &&
- except_specs.none? { |spec| spec.satisfied_by?(pipeline) }
- end
-
- selected_jobs.map { |_, job| build_attributes(job[:name]) }
+ def stage_builds_attributes(stage)
+ @jobs.values
+ .select { |job| job[:stage] == stage }
+ .map { |job| build_attributes(job[:name]) }
end
- def stage_seeds(pipeline)
- seeds = @stages.uniq.map do |stage|
- builds = pipeline_stage_builds(stage, pipeline)
+ def stages_attributes
+ @stages.uniq.map do |stage|
+ seeds = stage_builds_attributes(stage).map do |attributes|
+ job = @jobs.fetch(attributes[:name].to_sym)
- Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
- end
+ attributes
+ .merge(only: job.fetch(:only, {}))
+ .merge(except: job.fetch(:except, {}))
+ end
- seeds.compact
+ { name: stage, index: @stages.index(stage), builds: seeds }
+ end
end
- def self.validation_message(content)
+ def self.validation_message(content, opts = {})
return 'Please provide content of .gitlab-ci.yml' if content.blank?
begin
- Gitlab::Ci::YamlProcessor.new(content)
+ Gitlab::Ci::YamlProcessor.new(content, opts)
nil
rescue ValidationError => e
e.message
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index 3ccfd9a739d..65a65b67975 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -40,7 +40,10 @@ module Gitlab
# when there are no conflict files.
files.each(&:lines)
files.any?
- rescue Gitlab::Git::CommandError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
+ rescue Gitlab::Git::CommandError,
+ Gitlab::Git::Conflict::Parser::UnresolvableError,
+ Gitlab::Git::Conflict::Resolver::ConflictSideMissing,
+ Gitlab::Git::Conflict::File::UnsupportedEncoding
false
end
cache_method :can_be_resolved_in_ui?
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 21287a8efd0..1634fe4e9cb 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -59,6 +59,11 @@ module Gitlab
disable_statement_timeout
end
+ if index_exists?(table_name, column_name, options)
+ Rails.logger.warn "Index not created because it already exists (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}"
+ return
+ end
+
add_index(table_name, column_name, options)
end
@@ -83,6 +88,11 @@ module Gitlab
disable_statement_timeout
end
+ unless index_exists?(table_name, column_name, options)
+ Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, column_name: #{column_name}"
+ return
+ end
+
remove_index(table_name, options.merge({ column: column_name }))
end
@@ -107,6 +117,11 @@ module Gitlab
disable_statement_timeout
end
+ unless index_exists_by_name?(table_name, index_name)
+ Rails.logger.warn "Index not removed because it does not exist (this may be due to an aborted migration or similar): table_name: #{table_name}, index_name: #{index_name}"
+ return
+ end
+
remove_index(table_name, options.merge({ name: index_name }))
end
@@ -140,6 +155,13 @@ module Gitlab
# of PostgreSQL's "VALIDATE CONSTRAINT". As a result we'll just fall
# back to the normal foreign key procedure.
if Database.mysql?
+ if foreign_key_exists?(source, target, column: column)
+ Rails.logger.warn "Foreign key not created because it exists already " \
+ "(this may be due to an aborted migration or similar): " \
+ "source: #{source}, target: #{target}, column: #{column}"
+ return
+ end
+
return add_foreign_key(source, target,
column: column,
on_delete: on_delete)
@@ -151,25 +173,43 @@ module Gitlab
key_name = concurrent_foreign_key_name(source, column)
- # Using NOT VALID allows us to create a key without immediately
- # validating it. This means we keep the ALTER TABLE lock only for a
- # short period of time. The key _is_ enforced for any newly created
- # data.
- execute <<-EOF.strip_heredoc
- ALTER TABLE #{source}
- ADD CONSTRAINT #{key_name}
- FOREIGN KEY (#{column})
- REFERENCES #{target} (id)
- #{on_delete ? "ON DELETE #{on_delete.upcase}" : ''}
- NOT VALID;
- EOF
+ unless foreign_key_exists?(source, target, column: column)
+ Rails.logger.warn "Foreign key not created because it exists already " \
+ "(this may be due to an aborted migration or similar): " \
+ "source: #{source}, target: #{target}, column: #{column}"
+
+ # Using NOT VALID allows us to create a key without immediately
+ # validating it. This means we keep the ALTER TABLE lock only for a
+ # short period of time. The key _is_ enforced for any newly created
+ # data.
+ execute <<-EOF.strip_heredoc
+ ALTER TABLE #{source}
+ ADD CONSTRAINT #{key_name}
+ FOREIGN KEY (#{column})
+ REFERENCES #{target} (id)
+ #{on_delete ? "ON DELETE #{on_delete.upcase}" : ''}
+ NOT VALID;
+ EOF
+ end
# Validate the existing constraint. This can potentially take a very
# long time to complete, but fortunately does not lock the source table
# while running.
+ #
+ # Note this is a no-op in case the constraint is VALID already
execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};")
end
+ def foreign_key_exists?(source, target = nil, column: nil)
+ foreign_keys(source).any? do |key|
+ if column
+ key.options[:column].to_s == column.to_s
+ else
+ key.to_table.to_s == target.to_s
+ end
+ end
+ end
+
# Returns the name for a concurrent foreign key.
#
# PostgreSQL constraint names have a limit of 63 bytes. The logic used
@@ -860,17 +900,42 @@ into similar problems in the future (e.g. when new tables are created).
end
end
- def foreign_key_exists?(table, column)
- foreign_keys(table).any? do |key|
- key.options[:column] == column.to_s
+ # Fetches indexes on a column by name for postgres.
+ #
+ # This will include indexes using an expression on the column, for example:
+ # `CREATE INDEX CONCURRENTLY index_name ON table (LOWER(column));`
+ #
+ # For mysql, it falls back to the default ActiveRecord implementation that
+ # will not find custom indexes. But it will select by name without passing
+ # a column.
+ #
+ # We can remove this when upgrading to Rails 5 with an updated `index_exists?`:
+ # - https://github.com/rails/rails/commit/edc2b7718725016e988089b5fb6d6fb9d6e16882
+ #
+ # Or this can be removed when we no longer support postgres < 9.5, so we
+ # can use `CREATE INDEX IF NOT EXISTS`.
+ def index_exists_by_name?(table, index)
+ # We can't fall back to the normal `index_exists?` method because that
+ # does not find indexes without passing a column name.
+ if indexes(table).map(&:name).include?(index.to_s)
+ true
+ elsif Gitlab::Database.postgresql?
+ postgres_exists_by_name?(table, index)
+ else
+ false
end
end
- # Rails' index_exists? doesn't work when you only give it a table and index
- # name. As such we have to use some extra code to check if an index exists for
- # a given name.
- def index_exists_by_name?(table, index)
- indexes(table).map(&:name).include?(index)
+ def postgres_exists_by_name?(table, name)
+ index_sql = <<~SQL
+ SELECT COUNT(*)
+ FROM pg_index
+ JOIN pg_class i ON (indexrelid=i.oid)
+ JOIN pg_class t ON (indrelid=t.oid)
+ WHERE i.relname = '#{name}' AND t.relname = '#{table}'
+ SQL
+
+ connection.select_value(index_sql).to_i > 0
end
end
end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
index fd4a8832ec2..62d4d0a92a6 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
@@ -74,7 +74,7 @@ module Gitlab
}.freeze
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]['path']
+ Gitlab.config.repositories.storages[repository_storage].legacy_disk_path
end
# Overridden to have the correct `source_type` for the `route` relation
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 34b070dd375..014854da55c 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -27,8 +27,8 @@ module Gitlab
@fallback_diff_refs = fallback_diff_refs
# Ensure items are collected in the the batch
- new_blob
- old_blob
+ new_blob_lazy
+ old_blob_lazy
end
def position(position_marker, position_type: :text)
@@ -101,25 +101,19 @@ module Gitlab
end
def new_blob
- return unless new_content_sha
-
- Blob.lazy(repository.project, new_content_sha, file_path)
+ new_blob_lazy&.itself
end
def old_blob
- return unless old_content_sha
-
- Blob.lazy(repository.project, old_content_sha, old_path)
+ old_blob_lazy&.itself
end
def content_sha
new_content_sha || old_content_sha
end
- # Use #itself to check the value wrapped by a BatchLoader instance, rather
- # than if the BatchLoader instance itself is falsey.
def blob
- new_blob&.itself || old_blob&.itself
+ new_blob || old_blob
end
attr_writer :highlighted_diff_lines
@@ -237,17 +231,14 @@ module Gitlab
private
- # The blob instances are instances of BatchLoader, which means calling
- # &. directly on them won't work. Object#try also won't work, because Blob
- # doesn't inherit from Object, but from BasicObject (via SimpleDelegator).
+ # We can't use Object#try because Blob doesn't inherit from Object, but
+ # from BasicObject (via SimpleDelegator).
def try_blobs(meth)
- old_blob&.itself&.public_send(meth) || new_blob&.itself&.public_send(meth)
+ old_blob&.public_send(meth) || new_blob&.public_send(meth)
end
- # We can't use #compact for the same reason we can't use &., but calling
- # #nil? explicitly does work because it is proxied to the blob itself.
def valid_blobs
- [old_blob, new_blob].reject(&:nil?)
+ [old_blob, new_blob].compact
end
def text_position_properties(line)
@@ -262,6 +253,18 @@ module Gitlab
old_blob && new_blob && old_blob.id != new_blob.id
end
+ def new_blob_lazy
+ return unless new_content_sha
+
+ Blob.lazy(repository.project, new_content_sha, file_path)
+ end
+
+ def old_blob_lazy
+ return unless old_content_sha
+
+ Blob.lazy(repository.project, old_content_sha, old_path)
+ end
+
def simple_viewer_class
return DiffViewer::NotDiffable unless diffable?
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index 6659efa0961..0b8f6cfe3cb 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -90,7 +90,7 @@ module Gitlab
end
def clean(message)
- message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
+ message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "".encode("UTF-16BE"))
.encode("UTF-8")
.gsub("\0".encode("UTF-8"), "")
end
diff --git a/lib/gitlab/git/conflict/file.rb b/lib/gitlab/git/conflict/file.rb
index 2a9cf10a068..f08dab59ce4 100644
--- a/lib/gitlab/git/conflict/file.rb
+++ b/lib/gitlab/git/conflict/file.rb
@@ -2,17 +2,19 @@ module Gitlab
module Git
module Conflict
class File
+ UnsupportedEncoding = Class.new(StandardError)
+
attr_reader :their_path, :our_path, :our_mode, :repository, :commit_oid
- attr_accessor :content
+ attr_accessor :raw_content
- def initialize(repository, commit_oid, conflict, content)
+ def initialize(repository, commit_oid, conflict, raw_content)
@repository = repository
@commit_oid = commit_oid
@their_path = conflict[:theirs][:path]
@our_path = conflict[:ours][:path]
@our_mode = conflict[:ours][:mode]
- @content = content
+ @raw_content = raw_content
end
def lines
@@ -29,6 +31,14 @@ module Gitlab
end
end
+ def content
+ @content ||= @raw_content.dup.force_encoding('UTF-8')
+
+ raise UnsupportedEncoding unless @content.valid_encoding?
+
+ @content
+ end
+
def type
lines unless @type
diff --git a/lib/gitlab/git/conflict/parser.rb b/lib/gitlab/git/conflict/parser.rb
index 3effa9d2d31..fb5717dd556 100644
--- a/lib/gitlab/git/conflict/parser.rb
+++ b/lib/gitlab/git/conflict/parser.rb
@@ -4,7 +4,6 @@ module Gitlab
class Parser
UnresolvableError = Class.new(StandardError)
UnmergeableFile = Class.new(UnresolvableError)
- UnsupportedEncoding = Class.new(UnresolvableError)
# Recoverable errors - the conflict can be resolved in an editor, but not with
# sections.
@@ -75,10 +74,6 @@ module Gitlab
def validate_text!(text)
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 200.kilobytes
-
- text.force_encoding('UTF-8')
-
- raise UnsupportedEncoding unless text.valid_encoding?
end
def validate_delimiter!(condition)
diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb
index a142ed6b2ef..dc0bc8518bc 100644
--- a/lib/gitlab/git/gitlab_projects.rb
+++ b/lib/gitlab/git/gitlab_projects.rb
@@ -212,7 +212,7 @@ module Gitlab
end
def shard_name_from_shard_path(shard_path)
- Gitlab.config.repositories.storages.find { |_, info| info['path'] == shard_path }&.first ||
+ Gitlab.config.repositories.storages.find { |_, info| info.legacy_disk_path == shard_path }&.first ||
raise(ShardNameNotFoundError, "no shard found for path '#{shard_path}'")
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 9811c447a01..2d16a81c888 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -8,6 +8,7 @@ module Gitlab
class Repository
include Gitlab::Git::RepositoryMirroring
include Gitlab::Git::Popen
+ include Gitlab::EncodingHelper
ALLOWED_OBJECT_DIRECTORIES_VARIABLES = %w[
GIT_OBJECT_DIRECTORY
@@ -93,7 +94,7 @@ module Gitlab
@relative_path = relative_path
@gl_repository = gl_repository
- storage_path = Gitlab.config.repositories.storages[@storage]['path']
+ storage_path = Gitlab.config.repositories.storages[@storage].legacy_disk_path
@gitlab_projects = Gitlab::Git::GitlabProjects.new(
storage_path,
relative_path,
@@ -516,10 +517,6 @@ module Gitlab
end
end
- def sha_from_ref(ref)
- rev_parse_target(ref).oid
- end
-
# Return the object that +revspec+ points to. If +revspec+ is an
# annotated tag, then return the tag's target instead.
def rev_parse_target(revspec)
@@ -1390,7 +1387,7 @@ module Gitlab
offset = 2
args = %W(grep -i -I -n -z --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
- run_git(args).first.scrub.split(/^--$/)
+ run_git(args).first.scrub.split(/^--\n/)
end
def can_be_merged?(source_sha, target_branch)
@@ -1483,7 +1480,7 @@ module Gitlab
names.lines.each do |line|
next unless line.start_with?(refs_prefix)
- refs << line.rstrip[left_slice_count..-1]
+ refs << encode_utf8(line.rstrip[left_slice_count..-1])
end
refs
@@ -2409,6 +2406,10 @@ module Gitlab
def rev_list_param(spec)
spec == :all ? ['--all'] : spec
end
+
+ def sha_from_ref(ref)
+ rev_parse_target(ref).oid
+ end
end
end
end
diff --git a/lib/gitlab/git/storage/checker.rb b/lib/gitlab/git/storage/checker.rb
index d3c37f82101..2f611cef37b 100644
--- a/lib/gitlab/git/storage/checker.rb
+++ b/lib/gitlab/git/storage/checker.rb
@@ -35,7 +35,7 @@ module Gitlab
def initialize(storage, logger = Rails.logger)
@storage = storage
config = Gitlab.config.repositories.storages[@storage]
- @storage_path = config['path']
+ @storage_path = config.legacy_disk_path
@logger = logger
@hostname = Gitlab::Environment.hostname
diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb
index 898bb1b65be..e35054466ff 100644
--- a/lib/gitlab/git/storage/circuit_breaker.rb
+++ b/lib/gitlab/git/storage/circuit_breaker.rb
@@ -25,7 +25,7 @@ module Gitlab
if !config.present?
NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Storage '#{storage}' is not configured"))
- elsif !config['path'].present?
+ elsif !config.legacy_disk_path.present?
NullCircuitBreaker.new(storage, hostname, error: Misconfiguration.new("Path for storage '#{storage}' is not configured"))
else
new(storage, hostname)
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index 52b44b9b3c5..8d82820915d 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -29,7 +29,6 @@ module Gitlab
@repository.gitaly_migrate(:wiki_write_page) do |is_enabled|
if is_enabled
gitaly_write_page(name, format, content, commit_details)
- gollum_wiki.clear_cache
else
gollum_write_page(name, format, content, commit_details)
end
@@ -40,7 +39,6 @@ module Gitlab
@repository.gitaly_migrate(:wiki_delete_page) do |is_enabled|
if is_enabled
gitaly_delete_page(page_path, commit_details)
- gollum_wiki.clear_cache
else
gollum_delete_page(page_path, commit_details)
end
@@ -51,7 +49,6 @@ module Gitlab
@repository.gitaly_migrate(:wiki_update_page) do |is_enabled|
if is_enabled
gitaly_update_page(page_path, title, format, content, commit_details)
- gollum_wiki.clear_cache
else
gollum_update_page(page_path, title, format, content, commit_details)
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 6400089a22f..0a2a23e835b 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -53,7 +53,7 @@ module Gitlab
ensure_project_on_push!(cmd, changes)
check_project_accessibility!
- check_project_moved!
+ add_project_moved_message!
check_repository_existence!
case cmd
@@ -125,16 +125,12 @@ module Gitlab
end
end
- def check_project_moved!
+ def add_project_moved_message!
return if redirected_path.nil?
project_moved = Checks::ProjectMoved.new(project, user, protocol, redirected_path)
- if project_moved.permanent_redirect?
- project_moved.add_message
- else
- raise ProjectMovedError, project_moved.message(rejected: true)
- end
+ project_moved.add_message
end
def check_command_disabled!(cmd)
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 8ca30ffc232..0abae70c443 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -83,6 +83,10 @@ module Gitlab
end
end
+ def self.random_storage
+ Gitlab.config.repositories.storages.keys.sample
+ end
+
def self.address(storage)
params = Gitlab.config.repositories.storages[storage]
raise "storage not found: #{storage.inspect}" if params.nil?
diff --git a/lib/gitlab/gitaly_client/conflict_files_stitcher.rb b/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
index 97c13d1fdb0..c275a065bce 100644
--- a/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
+++ b/lib/gitlab/gitaly_client/conflict_files_stitcher.rb
@@ -17,7 +17,7 @@ module Gitlab
current_file = file_from_gitaly_header(gitaly_file.header)
else
- current_file.content << gitaly_file.content
+ current_file.raw_content << gitaly_file.content
end
end
end
diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb
index 58c356edfd1..f2d699d9dfb 100644
--- a/lib/gitlab/gitaly_client/remote_service.rb
+++ b/lib/gitlab/gitaly_client/remote_service.rb
@@ -3,6 +3,17 @@ module Gitlab
class RemoteService
MAX_MSG_SIZE = 128.kilobytes.freeze
+ def self.exists?(remote_url)
+ request = Gitaly::FindRemoteRepositoryRequest.new(remote: remote_url)
+
+ response = GitalyClient.call(GitalyClient.random_storage,
+ :remote_service,
+ :find_remote_repository, request,
+ timeout: GitalyClient.medium_timeout)
+
+ response.exists
+ end
+
def initialize(repository)
@repository = repository
@gitaly_repo = repository.gitaly_repository
diff --git a/lib/gitlab/gitaly_client/storage_settings.rb b/lib/gitlab/gitaly_client/storage_settings.rb
new file mode 100644
index 00000000000..8668caf0c55
--- /dev/null
+++ b/lib/gitlab/gitaly_client/storage_settings.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module GitalyClient
+ # This is a chokepoint that is meant to help us stop remove all places
+ # where production code (app, config, db, lib) touches Git repositories
+ # directly.
+ class StorageSettings
+ DirectPathAccessError = Class.new(StandardError)
+
+ # This class will give easily recognizable NoMethodErrors
+ Deprecated = Class.new
+
+ attr_reader :legacy_disk_path
+
+ def initialize(storage)
+ raise "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash)
+
+ # Support a nil 'path' field because some of the circuit breaker tests use it.
+ @legacy_disk_path = File.expand_path(storage['path'], Rails.root) if storage['path']
+
+ storage['path'] = Deprecated
+ @hash = storage
+ end
+
+ def gitaly_address
+ @hash.fetch(:gitaly_address)
+ end
+
+ private
+
+ def method_missing(m, *args, &block)
+ @hash.public_send(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index ab0b751fe24..b1b283e98b5 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -16,7 +16,8 @@ module Gitlab
# Returns true if we should import the wiki for the project.
def import_wiki?
client.repository(project.import_source)&.has_wiki &&
- !project.wiki_repository_exists?
+ !project.wiki_repository_exists? &&
+ Gitlab::GitalyClient::RemoteService.exists?(wiki_url)
end
# Imports the repository data.
@@ -55,7 +56,6 @@ module Gitlab
def import_wiki_repository
wiki_path = "#{project.disk_path}.wiki"
- wiki_url = project.import_url.sub(/\.git\z/, '.wiki.git')
storage_path = project.repository_storage_path
gitlab_shell.import_repository(storage_path, wiki_path, wiki_url)
@@ -70,6 +70,10 @@ module Gitlab
end
end
+ def wiki_url
+ project.import_url.sub(/\.git\z/, '.wiki.git')
+ end
+
def update_clone_time
project.update_column(:last_repository_updated_at, Time.zone.now)
end
diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb
index afaa59b1018..6e554383270 100644
--- a/lib/gitlab/health_checks/fs_shards_check.rb
+++ b/lib/gitlab/health_checks/fs_shards_check.rb
@@ -77,7 +77,7 @@ module Gitlab
end
def storage_path(storage_name)
- storages_paths&.dig(storage_name, 'path')
+ storages_paths[storage_name]&.legacy_disk_path
end
# All below test methods use shell commands to perform actions on storage volumes.
diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb
new file mode 100644
index 00000000000..96558872a37
--- /dev/null
+++ b/lib/gitlab/http.rb
@@ -0,0 +1,11 @@
+# This class is used as a proxy for all outbounding http connection
+# coming from callbacks, services and hooks. The direct use of the HTTParty
+# is discouraged because it can lead to several security problems, like SSRF
+# calling internal IP or services.
+module Gitlab
+ class HTTP
+ include HTTParty # rubocop:disable Gitlab/HTTParty
+
+ connection_adapter ProxyHTTPConnectionAdapter
+ end
+end
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
index d9d5f90596f..7f63e39b3aa 100644
--- a/lib/gitlab/middleware/read_only.rb
+++ b/lib/gitlab/middleware/read_only.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def call(env)
- ReadOnly::Controller.new(@app, env).call
+ ::Gitlab::Middleware::ReadOnly::Controller.new(@app, env).call
end
end
end
diff --git a/lib/gitlab/omniauth_initializer.rb b/lib/gitlab/omniauth_initializer.rb
new file mode 100644
index 00000000000..35ed3a5ac05
--- /dev/null
+++ b/lib/gitlab/omniauth_initializer.rb
@@ -0,0 +1,75 @@
+module Gitlab
+ class OmniauthInitializer
+ def initialize(devise_config)
+ @devise_config = devise_config
+ end
+
+ def execute(providers)
+ providers.each do |provider|
+ add_provider(provider['name'].to_sym, *arguments_for(provider))
+ end
+ end
+
+ private
+
+ def add_provider(*args)
+ @devise_config.omniauth(*args)
+ end
+
+ def arguments_for(provider)
+ provider_arguments = []
+
+ %w[app_id app_secret].each do |argument|
+ provider_arguments << provider[argument] if provider[argument]
+ end
+
+ case provider['args']
+ when Array
+ # An Array from the configuration will be expanded.
+ provider_arguments.concat provider['args']
+ when Hash
+ hash_arguments = provider['args'].merge(provider_defaults(provider))
+
+ # A Hash from the configuration will be passed as is.
+ provider_arguments << hash_arguments.symbolize_keys
+ end
+
+ provider_arguments
+ end
+
+ def provider_defaults(provider)
+ case provider['name']
+ when 'cas3'
+ { on_single_sign_out: cas3_signout_handler }
+ when 'authentiq'
+ { remote_sign_out_handler: authentiq_signout_handler }
+ when 'shibboleth'
+ { fail_with_empty_uid: true }
+ else
+ {}
+ end
+ end
+
+ def cas3_signout_handler
+ lambda do |request|
+ ticket = request.params[:session_index]
+ raise "Service Ticket not found." unless Gitlab::Auth::OAuth::Session.valid?(:cas3, ticket)
+
+ Gitlab::Auth::OAuth::Session.destroy(:cas3, ticket)
+ true
+ end
+ end
+
+ def authentiq_signout_handler
+ lambda do |request|
+ authentiq_session = request.params['sid']
+ if Gitlab::Auth::OAuth::Session.valid?(:authentiq, authentiq_session)
+ Gitlab::Auth::OAuth::Session.destroy(:authentiq, authentiq_session)
+ true
+ else
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb
index 98a168b43bb..18540e64d4c 100644
--- a/lib/gitlab/profiler.rb
+++ b/lib/gitlab/profiler.rb
@@ -92,8 +92,8 @@ module Gitlab
if type && time
@load_times_by_model ||= {}
- @load_times_by_model[type] ||= 0
- @load_times_by_model[type] += time.to_f
+ @load_times_by_model[type] ||= []
+ @load_times_by_model[type] << time.to_f
end
super
@@ -135,8 +135,12 @@ module Gitlab
def self.log_load_times_by_model(logger)
return unless logger.respond_to?(:load_times_by_model)
- logger.load_times_by_model.to_a.sort_by(&:last).reverse.each do |(model, time)|
- logger.info("#{model} total: #{time.round(2)}ms")
+ summarised_load_times = logger.load_times_by_model.to_a.map do |(model, times)|
+ [model, times.count, times.sum]
+ end
+
+ summarised_load_times.sort_by(&:last).reverse.each do |(model, query_count, time)|
+ logger.info("#{model} total (#{query_count}): #{time.round(2)}ms")
end
end
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 29277ec6481..390efda326a 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -58,7 +58,7 @@ module Gitlab
data = ""
startline = 0
- result.strip.each_line.each_with_index do |line, index|
+ result.each_line.each_with_index do |line, index|
prefix ||= line.match(/^(?<ref>[^:]*):(?<filename>.*)\x00(?<startline>\d+)\x00/)&.tap do |matches|
ref = matches[:ref]
filename = matches[:filename]
diff --git a/lib/gitlab/proxy_http_connection_adapter.rb b/lib/gitlab/proxy_http_connection_adapter.rb
new file mode 100644
index 00000000000..c70d6f4cd84
--- /dev/null
+++ b/lib/gitlab/proxy_http_connection_adapter.rb
@@ -0,0 +1,34 @@
+# This class is part of the Gitlab::HTTP wrapper. Depending on the value
+# of the global setting allow_local_requests_from_hooks_and_services this adapter
+# will allow/block connection to internal IPs and/or urls.
+#
+# This functionality can be overriden by providing the setting the option
+# allow_local_requests = true in the request. For example:
+# Gitlab::HTTP.get('http://www.gitlab.com', allow_local_requests: true)
+#
+# This option will take precedence over the global setting.
+module Gitlab
+ class ProxyHTTPConnectionAdapter < HTTParty::ConnectionAdapter
+ def connection
+ if !allow_local_requests? && blocked_url?
+ raise URI::InvalidURIError
+ end
+
+ super
+ end
+
+ private
+
+ def blocked_url?
+ Gitlab::UrlBlocker.blocked_url?(uri, allow_private_networks: false)
+ end
+
+ def allow_local_requests?
+ options.fetch(:allow_local_requests, allow_settings_local_requests?)
+ end
+
+ def allow_settings_local_requests?
+ Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services?
+ end
+ end
+end
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
index 79265cf952d..1fa2a19b0af 100644
--- a/lib/gitlab/repo_path.rb
+++ b/lib/gitlab/repo_path.rb
@@ -21,11 +21,11 @@ module Gitlab
result = repo_path
storage = Gitlab.config.repositories.storages.values.find do |params|
- repo_path.start_with?(params['path'])
+ repo_path.start_with?(params.legacy_disk_path)
end
if storage
- result = result.sub(storage['path'], '')
+ result = result.sub(storage.legacy_disk_path, '')
elsif fail_on_not_found
raise NotFoundError.new("No known storage path matches #{repo_path.inspect}")
end
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index 07d7c91cb5d..e5c02dd8ecc 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -24,7 +24,7 @@ module Gitlab
address = val['gitaly_address']
end
- storages << { name: key, path: val['path'] }
+ storages << { name: key, path: val.legacy_disk_path }
end
if Rails.env.test?
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 3a8f5826818..c8c15b9684a 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -82,7 +82,7 @@ module Gitlab
repository.gitaly_repository_client.create_repository
true
else
- repo_path = File.join(Gitlab.config.repositories.storages[storage]['path'], relative_path)
+ repo_path = File.join(Gitlab.config.repositories.storages[storage].legacy_disk_path, relative_path)
Gitlab::Git::Repository.create(repo_path, bare: true, symlink_hooks_to: gitlab_shell_hooks_path)
end
end
@@ -131,7 +131,7 @@ module Gitlab
if is_enabled
repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, timeout: git_timeout, prune: prune)
else
- storage_path = Gitlab.config.repositories.storages[repository.storage]["path"]
+ storage_path = Gitlab.config.repositories.storages[repository.storage].legacy_disk_path
local_fetch_remote(storage_path, repository.relative_path, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune)
end
end
@@ -478,7 +478,7 @@ module Gitlab
def gitaly_namespace_client(storage_path)
storage, _value = Gitlab.config.repositories.storages.find do |storage, value|
- value['path'] == storage_path
+ value.legacy_disk_path == storage_path
end
Gitlab::GitalyClient::NamespaceService.new(storage)
diff --git a/lib/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb
index 34bee6fecbe..42be301fd9b 100644
--- a/lib/gitlab/task_helpers.rb
+++ b/lib/gitlab/task_helpers.rb
@@ -129,7 +129,7 @@ module Gitlab
def all_repos
Gitlab.config.repositories.storages.each_value do |repository_storage|
- IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -type d -name *.git)) do |find|
+ IO.popen(%W(find #{repository_storage.legacy_disk_path} -mindepth 2 -type d -name *.git)) do |find|
find.each_line do |path|
yield path.chomp
end
@@ -138,7 +138,7 @@ module Gitlab
end
def repository_storage_paths_args
- Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
+ Gitlab.config.repositories.storages.values.map { |rs| rs.legacy_disk_path }
end
def user_home
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 13150ddab67..0f9f939e204 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -3,11 +3,7 @@ require 'resolv'
module Gitlab
class UrlBlocker
class << self
- # Used to specify what hosts and port numbers should be prohibited for project
- # imports.
- VALID_PORTS = [22, 80, 443].freeze
-
- def blocked_url?(url)
+ def blocked_url?(url, allow_private_networks: true, valid_ports: [])
return false if url.nil?
blocked_ips = ["127.0.0.1", "::1", "0.0.0.0"]
@@ -18,12 +14,15 @@ module Gitlab
# Allow imports from the GitLab instance itself but only from the configured ports
return false if internal?(uri)
- return true if blocked_port?(uri.port)
+ return true if blocked_port?(uri.port, valid_ports)
return true if blocked_user_or_hostname?(uri.user)
return true if blocked_user_or_hostname?(uri.hostname)
- server_ips = Addrinfo.getaddrinfo(uri.hostname, 80, nil, :STREAM).map(&:ip_address)
+ addrs_info = Addrinfo.getaddrinfo(uri.hostname, 80, nil, :STREAM)
+ server_ips = addrs_info.map(&:ip_address)
+
return true if (blocked_ips & server_ips).any?
+ return true if !allow_private_networks && private_network?(addrs_info)
rescue Addressable::URI::InvalidURIError
return true
rescue SocketError
@@ -35,10 +34,10 @@ module Gitlab
private
- def blocked_port?(port)
- return false if port.blank?
+ def blocked_port?(port, valid_ports)
+ return false if port.blank? || valid_ports.blank?
- port < 1024 && !VALID_PORTS.include?(port)
+ port < 1024 && !valid_ports.include?(port)
end
def blocked_user_or_hostname?(value)
@@ -61,6 +60,10 @@ module Gitlab
(uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
end
+ def private_network?(addrs_info)
+ addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }
+ end
+
def config
Gitlab.config
end
diff --git a/lib/gitlab/verify/lfs_objects.rb b/lib/gitlab/verify/lfs_objects.rb
index fe51edbdeeb..970e2a7b718 100644
--- a/lib/gitlab/verify/lfs_objects.rb
+++ b/lib/gitlab/verify/lfs_objects.rb
@@ -12,7 +12,7 @@ module Gitlab
private
def relation
- LfsObject.all
+ LfsObject.with_files_stored_locally
end
def expected_checksum(lfs_object)
diff --git a/lib/gitlab/verify/uploads.rb b/lib/gitlab/verify/uploads.rb
index 6972e517ea5..0ffa71a6d72 100644
--- a/lib/gitlab/verify/uploads.rb
+++ b/lib/gitlab/verify/uploads.rb
@@ -12,7 +12,7 @@ module Gitlab
private
def relation
- Upload.all
+ Upload.with_files_stored_locally
end
def expected_checksum(upload)
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 0b0d667d4fd..a3012eca04c 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -21,12 +21,10 @@ module Gitlab
raise "Unsupported action: #{action}" unless ALLOWED_GIT_HTTP_ACTIONS.include?(action.to_s)
project = repository.project
- repo_path = repository.path_to_repo
params = {
GL_ID: Gitlab::GlId.gl_id(user),
GL_REPOSITORY: Gitlab::GlRepository.gl_repository(project, is_wiki),
GL_USERNAME: user&.username,
- RepoPath: repo_path,
ShowAllRefs: show_all_refs
}
server = {
@@ -39,20 +37,12 @@ module Gitlab
params
end
- def lfs_upload_ok(oid, size)
- {
- StoreLFSPath: LfsObjectUploader.workhorse_upload_path,
- LfsOid: oid,
- LfsSize: size
- }
- end
-
def artifact_upload_ok
{ TempPath: JobArtifactUploader.workhorse_upload_path }
end
def send_git_blob(repository, blob)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show)
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
'GitalyServer' => gitaly_server_hash(repository),
'GetBlobRequest' => {
@@ -80,7 +70,7 @@ module Gitlab
params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format)
raise "Repository or ref not found" if params.empty?
- if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive)
+ if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
params.merge!(
'GitalyServer' => gitaly_server_hash(repository),
'GitalyRepository' => repository.gitaly_repository.to_h
@@ -97,7 +87,7 @@ module Gitlab
end
def send_git_diff(repository, diff_refs)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff)
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
'GitalyServer' => gitaly_server_hash(repository),
'RawDiffRequest' => Gitaly::RawDiffRequest.new(
@@ -115,7 +105,7 @@ module Gitlab
end
def send_git_patch(repository, diff_refs)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch)
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
{
'GitalyServer' => gitaly_server_hash(repository),
'RawPatchRequest' => Gitaly::RawPatchRequest.new(
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index 65ccdb3c347..85f78e44f32 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -22,16 +22,14 @@ module Mattermost
# going.
class Session
include Doorkeeper::Helpers::Controller
- include HTTParty
LEASE_TIMEOUT = 60
- base_uri Settings.mattermost.host
-
- attr_accessor :current_resource_owner, :token
+ attr_accessor :current_resource_owner, :token, :base_uri
def initialize(current_user)
@current_resource_owner = current_user
+ @base_uri = Settings.mattermost.host
end
def with_session
@@ -73,24 +71,32 @@ module Mattermost
def get(path, options = {})
handle_exceptions do
- self.class.get(path, options.merge(headers: @headers))
+ Gitlab::HTTP.get(path, build_options(options))
end
end
def post(path, options = {})
handle_exceptions do
- self.class.post(path, options.merge(headers: @headers))
+ Gitlab::HTTP.post(path, build_options(options))
end
end
def delete(path, options = {})
handle_exceptions do
- self.class.delete(path, options.merge(headers: @headers))
+ Gitlab::HTTP.delete(path, build_options(options))
end
end
private
+ def build_options(options)
+ options.tap do |hash|
+ hash[:headers] = @headers
+ hash[:allow_local_requests] = true
+ hash[:base_uri] = base_uri if base_uri.presence
+ end
+ end
+
def create
raise Mattermost::NoSessionError unless oauth_uri
raise Mattermost::NoSessionError unless token_uri
@@ -165,14 +171,14 @@ module Mattermost
def handle_exceptions
yield
- rescue HTTParty::Error => e
+ rescue Gitlab::HTTP::Error => e
raise Mattermost::ConnectionError.new(e.message)
rescue Errno::ECONNREFUSED => e
raise Mattermost::ConnectionError.new(e.message)
end
def parse_cookie(response)
- cookie_hash = CookieHash.new
+ cookie_hash = Gitlab::HTTP::CookieHash.new
response.get_fields('Set-Cookie').each { |c| cookie_hash.add_cookies(c) }
cookie_hash
end
diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb
index 3bef68a1bcb..c08d3e933a8 100644
--- a/lib/microsoft_teams/notifier.rb
+++ b/lib/microsoft_teams/notifier.rb
@@ -9,14 +9,15 @@ module MicrosoftTeams
result = false
begin
- response = HTTParty.post(
+ response = Gitlab::HTTP.post(
@webhook.to_str,
headers: @header,
+ allow_local_requests: true,
body: body(options)
)
result = true if response
- rescue HTTParty::Error, StandardError => error
+ rescue Gitlab::HTTP::Error, StandardError => error
Rails.logger.info("#{self.class.name}: Error while connecting to #{@webhook}: #{error.message}")
end
diff --git a/lib/peek/views/host.rb b/lib/peek/views/host.rb
new file mode 100644
index 00000000000..43c8a35c7ea
--- /dev/null
+++ b/lib/peek/views/host.rb
@@ -0,0 +1,9 @@
+module Peek
+ module Views
+ class Host < View
+ def results
+ { hostname: Gitlab::Environment.hostname }
+ end
+ end
+ end
+end
diff --git a/lib/system_check/orphans/namespace_check.rb b/lib/system_check/orphans/namespace_check.rb
index b8446300f72..b5f443abe06 100644
--- a/lib/system_check/orphans/namespace_check.rb
+++ b/lib/system_check/orphans/namespace_check.rb
@@ -6,8 +6,8 @@ module SystemCheck
def multi_check
Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
$stdout.puts
- $stdout.puts "* Storage: #{storage_name} (#{repository_storage['path']})".color(:yellow)
- toplevel_namespace_dirs = disk_namespaces(repository_storage['path'])
+ $stdout.puts "* Storage: #{storage_name} (#{repository_storage.legacy_disk_path})".color(:yellow)
+ toplevel_namespace_dirs = disk_namespaces(repository_storage.legacy_disk_path)
orphans = (toplevel_namespace_dirs - existing_namespaces)
print_orphans(orphans, storage_name)
diff --git a/lib/system_check/orphans/repository_check.rb b/lib/system_check/orphans/repository_check.rb
index 9b6b2429783..5ef0b93ad08 100644
--- a/lib/system_check/orphans/repository_check.rb
+++ b/lib/system_check/orphans/repository_check.rb
@@ -6,10 +6,12 @@ module SystemCheck
def multi_check
Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
+ storage_path = repository_storage.legacy_disk_path
+
$stdout.puts
- $stdout.puts "* Storage: #{storage_name} (#{repository_storage['path']})".color(:yellow)
+ $stdout.puts "* Storage: #{storage_name} (#{storage_path})".color(:yellow)
- repositories = disk_repositories(repository_storage['path'])
+ repositories = disk_repositories(storage_path)
orphans = (repositories - fetch_repositories(storage_name))
print_orphans(orphans, storage_name)
diff --git a/lib/tasks/gitlab/artifacts/migrate.rake b/lib/tasks/gitlab/artifacts/migrate.rake
new file mode 100644
index 00000000000..bfca4bfb3f7
--- /dev/null
+++ b/lib/tasks/gitlab/artifacts/migrate.rake
@@ -0,0 +1,25 @@
+require 'logger'
+require 'resolv-replace'
+
+desc "GitLab | Migrate files for artifacts to comply with new storage format"
+namespace :gitlab do
+ namespace :artifacts do
+ task migrate: :environment do
+ logger = Logger.new(STDOUT)
+ logger.info('Starting transfer of artifacts')
+
+ Ci::Build.joins(:project)
+ .with_artifacts_stored_locally
+ .find_each(batch_size: 10) do |build|
+ begin
+ build.artifacts_file.migrate!(ObjectStorage::Store::REMOTE)
+ build.artifacts_metadata.migrate!(ObjectStorage::Store::REMOTE)
+
+ logger.info("Transferred artifacts of #{build.id} of #{build.artifacts_size} to object storage")
+ rescue => e
+ logger.error("Failed to transfer artifacts of #{build.id} with error: #{e.message}")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 2403f57f05a..abef8cd2bcc 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -61,7 +61,7 @@ namespace :gitlab do
puts "Repo base directory exists?"
Gitlab.config.repositories.storages.each do |name, repository_storage|
- repo_base_path = repository_storage['path']
+ repo_base_path = repository_storage.legacy_disk_path
print "#{name}... "
if File.exist?(repo_base_path)
@@ -86,7 +86,7 @@ namespace :gitlab do
puts "Repo storage directories are symlinks?"
Gitlab.config.repositories.storages.each do |name, repository_storage|
- repo_base_path = repository_storage['path']
+ repo_base_path = repository_storage.legacy_disk_path
print "#{name}... "
unless File.exist?(repo_base_path)
@@ -110,7 +110,7 @@ namespace :gitlab do
puts "Repo paths access is drwxrws---?"
Gitlab.config.repositories.storages.each do |name, repository_storage|
- repo_base_path = repository_storage['path']
+ repo_base_path = repository_storage.legacy_disk_path
print "#{name}... "
unless File.exist?(repo_base_path)
@@ -140,7 +140,7 @@ namespace :gitlab do
puts "Repo paths owned by #{gitlab_shell_ssh_user}:root, or #{gitlab_shell_ssh_user}:#{Gitlab.config.gitlab_shell.owner_group}?"
Gitlab.config.repositories.storages.each do |name, repository_storage|
- repo_base_path = repository_storage['path']
+ repo_base_path = repository_storage.legacy_disk_path
print "#{name}... "
unless File.exist?(repo_base_path)
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 2453079911d..d6d15285489 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -12,7 +12,7 @@ namespace :gitlab do
namespaces = Namespace.pluck(:path)
namespaces << HASHED_REPOSITORY_NAME # add so that it will be ignored
Gitlab.config.repositories.storages.each do |name, repository_storage|
- git_base_path = repository_storage['path']
+ git_base_path = repository_storage.legacy_disk_path
all_dirs = Dir.glob(git_base_path + '/*')
puts git_base_path.color(:yellow)
@@ -54,7 +54,7 @@ namespace :gitlab do
move_suffix = "+orphaned+#{Time.now.to_i}"
Gitlab.config.repositories.storages.each do |name, repository_storage|
- repo_root = repository_storage['path']
+ repo_root = repository_storage.legacy_disk_path
# Look for global repos (legacy, depth 1) and normal repos (depth 2)
IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
find.each_line do |path|
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index 45e9a1a1c72..47ed522aec3 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -68,7 +68,7 @@ namespace :gitlab do
puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}"
puts "Repository storage paths:"
Gitlab.config.repositories.storages.each do |name, repository_storage|
- puts "- #{name}: \t#{repository_storage['path']}"
+ puts "- #{name}: \t#{repository_storage.legacy_disk_path}"
end
puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}"
puts "Git:\t\t#{Gitlab.config.git.bin_path}"
diff --git a/lib/tasks/gitlab/lfs/migrate.rake b/lib/tasks/gitlab/lfs/migrate.rake
new file mode 100644
index 00000000000..a45e5ca91e0
--- /dev/null
+++ b/lib/tasks/gitlab/lfs/migrate.rake
@@ -0,0 +1,22 @@
+require 'logger'
+
+desc "GitLab | Migrate LFS objects to remote storage"
+namespace :gitlab do
+ namespace :lfs do
+ task migrate: :environment do
+ logger = Logger.new(STDOUT)
+ logger.info('Starting transfer of LFS files to object storage')
+
+ LfsObject.with_files_stored_locally
+ .find_each(batch_size: 10) do |lfs_object|
+ begin
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+
+ logger.info("Transferred LFS object #{lfs_object.oid} of size #{lfs_object.size.to_i.bytes} to object storage")
+ rescue => e
+ logger.error("Failed to transfer LFS object #{lfs_object.oid} with error: #{e.message}")
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/uploads/migrate.rake b/lib/tasks/gitlab/uploads/migrate.rake
new file mode 100644
index 00000000000..c26c3ccb3be
--- /dev/null
+++ b/lib/tasks/gitlab/uploads/migrate.rake
@@ -0,0 +1,33 @@
+namespace :gitlab do
+ namespace :uploads do
+ desc 'GitLab | Uploads | Migrate the uploaded files to object storage'
+ task :migrate, [:uploader_class, :model_class, :mounted_as] => :environment do |task, args|
+ batch_size = ENV.fetch('BATCH', 200).to_i
+ @to_store = ObjectStorage::Store::REMOTE
+ @mounted_as = args.mounted_as&.gsub(':', '')&.to_sym
+ @uploader_class = args.uploader_class.constantize
+ @model_class = args.model_class.constantize
+
+ uploads.each_batch(of: batch_size, &method(:enqueue_batch)) # rubocop: disable Cop/InBatches
+ end
+
+ def enqueue_batch(batch, index)
+ job = ObjectStorage::MigrateUploadsWorker.enqueue!(batch,
+ @mounted_as,
+ @to_store)
+ puts "Enqueued job ##{index}: #{job}"
+ rescue ObjectStorage::MigrateUploadsWorker::SanityCheckError => e
+ # continue for the next batch
+ puts "Could not enqueue batch (#{batch.ids}) #{e.message}".color(:red)
+ end
+
+ def uploads
+ Upload.class_eval { include EachBatch } unless Upload < EachBatch
+
+ Upload
+ .where.not(store: @to_store)
+ .where(uploader: @uploader_class.to_s,
+ model_type: @model_class.base_class.sti_name)
+ end
+ end
+end
diff --git a/lib/tasks/haml-lint.rake b/lib/tasks/haml-lint.rake
index 5c0cc4990fc..ad2d034b0b4 100644
--- a/lib/tasks/haml-lint.rake
+++ b/lib/tasks/haml-lint.rake
@@ -2,14 +2,5 @@ unless Rails.env.production?
require 'haml_lint/rake_task'
require 'haml_lint/inline_javascript'
- # Workaround for warnings from parser/current
- # TODO: Remove this after we update parser gem
- task :haml_lint do
- require 'parser'
- def Parser.warn(*args)
- puts(*args) # static-analysis ignores stdout if status is 0
- end
- end
-
HamlLint::RakeTask.new
end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index 1c7a8a90f5c..af30ecb0e9b 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -7,8 +7,8 @@ task setup_postgresql: :environment do
require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes')
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
- require Rails.root.join('db/migrate/20180113220114_rework_redirect_routes_indexes.rb')
require Rails.root.join('db/migrate/20180215181245_users_name_lower_index.rb')
+ require Rails.root.join('db/post_migrate/20180306164012_add_path_index_to_redirect_routes.rb')
NamespacesProjectsPathLowerIndexes.new.up
AddUsersLowerUsernameEmailIndexes.new.up
@@ -17,6 +17,6 @@ task setup_postgresql: :environment do
AddLowerPathIndexToRedirectRoutes.new.up
IndexRedirectRoutesPathForLike.new.up
AddIndexOnNamespacesLowerName.new.up
- ReworkRedirectRoutesIndexes.new.up
UsersNameLowerIndex.new.up
+ AddPathIndexToRedirectRoutes.new.up
end
diff --git a/package.json b/package.json
index deee668ae3b..31edc3a8016 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
- "@gitlab-org/gitlab-svgs": "^1.14.0",
+ "@gitlab-org/gitlab-svgs": "^1.16.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
"babel-core": "^6.26.0",
@@ -107,6 +107,7 @@
"eslint-plugin-jasmine": "^2.1.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-vue": "^4.0.1",
+ "ignore": "^3.3.7",
"istanbul": "^0.4.5",
"jasmine-core": "^2.9.0",
"jasmine-jquery": "^2.1.1",
@@ -120,5 +121,8 @@
"nodemon": "^1.15.1",
"prettier": "1.11.1",
"webpack-dev-server": "^2.11.2"
+ },
+ "optionalDependencies": {
+ "fsevents": "^1.1.3"
}
}
diff --git a/public/robots.txt b/public/robots.txt
index 123272a9834..1f9d42f4adc 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -20,6 +20,7 @@ Disallow: /projects/new
Disallow: /groups/new
Disallow: /groups/*/edit
Disallow: /users
+Disallow: /help
# Global snippets
User-Agent: *
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index 35875487da8..2f2506f08fb 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -4,6 +4,7 @@ module QA
class Show < Page::Base
view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js' do
element :merge_button
+ element :fast_forward_message, 'Fast-forward merge without a merge commit'
end
view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue' do
@@ -12,19 +13,19 @@ module QA
view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue' do
element :mr_rebase_button
- element :fast_forward_nessage, "Fast-forward merge is not possible"
+ element :no_fast_forward_message, 'Fast-forward merge is not possible'
end
def rebase!
- wait(reload: false) do
- click_element :mr_rebase_button
+ click_element :mr_rebase_button
- has_text?("The source branch HEAD has recently changed.")
+ wait(reload: false) do
+ has_text?('Fast-forward merge without a merge commit')
end
end
def fast_forward_possible?
- !has_text?("Fast-forward merge is not possible")
+ !has_text?('Fast-forward merge is not possible')
end
def has_merge_button?
@@ -34,10 +35,10 @@ module QA
end
def merge!
- wait(reload: false) do
- click_element :merge_button
+ click_element :merge_button
- has_text?("The changes were merged into")
+ wait(reload: false) do
+ has_text?('The changes were merged into')
end
end
end
diff --git a/rubocop/cop/gitlab/httparty.rb b/rubocop/cop/gitlab/httparty.rb
new file mode 100644
index 00000000000..215f18b6993
--- /dev/null
+++ b/rubocop/cop/gitlab/httparty.rb
@@ -0,0 +1,62 @@
+require_relative '../../spec_helpers'
+
+module RuboCop
+ module Cop
+ module Gitlab
+ class HTTParty < RuboCop::Cop::Cop
+ include SpecHelpers
+
+ MSG_SEND = <<~EOL.freeze
+ Avoid calling `HTTParty` directly. Instead, use the Gitlab::HTTP
+ wrapper. To allow request to localhost or the private network set
+ the option :allow_local_requests in the request call.
+ EOL
+
+ MSG_INCLUDE = <<~EOL.freeze
+ Avoid including `HTTParty` directly. Instead, use the Gitlab::HTTP
+ wrapper. To allow request to localhost or the private network set
+ the option :allow_local_requests in the request call.
+ EOL
+
+ def_node_matcher :includes_httparty?, <<~PATTERN
+ (send nil? :include (const nil? :HTTParty))
+ PATTERN
+
+ def_node_matcher :httparty_node?, <<~PATTERN
+ (send (const nil? :HTTParty)...)
+ PATTERN
+
+ def on_send(node)
+ return if in_spec?(node)
+
+ add_offense(node, location: :expression, message: MSG_SEND) if httparty_node?(node)
+ add_offense(node, location: :expression, message: MSG_INCLUDE) if includes_httparty?(node)
+ end
+
+ def autocorrect(node)
+ if includes_httparty?(node)
+ autocorrect_includes_httparty(node)
+ else
+ autocorrect_httparty_node(node)
+ end
+ end
+
+ def autocorrect_includes_httparty(node)
+ lambda do |corrector|
+ corrector.remove(node.source_range)
+ end
+ end
+
+ def autocorrect_httparty_node(node)
+ _, method_name, *arg_nodes = *node
+
+ replacement = "Gitlab::HTTP.#{method_name}(#{arg_nodes.map(&:source).join(', ')})"
+
+ lambda do |corrector|
+ corrector.replace(node.source_range, replacement)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index b36a3f9c8a0..0b4c7d31442 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,6 +1,7 @@
# rubocop:disable Naming/FileName
require_relative 'cop/gitlab/module_with_instance_variables'
require_relative 'cop/gitlab/predicate_memoization'
+require_relative 'cop/gitlab/httparty'
require_relative 'cop/include_sidekiq_worker'
require_relative 'cop/line_break_around_conditional_block'
require_relative 'cop/migration/add_column'
diff --git a/scripts/frontend/frontend_script_utils.js b/scripts/frontend/frontend_script_utils.js
index 2c06747255c..e42b912d359 100644
--- a/scripts/frontend/frontend_script_utils.js
+++ b/scripts/frontend/frontend_script_utils.js
@@ -1,4 +1,3 @@
-/* eslint import/no-commonjs: "off" */
const execFileSync = require('child_process').execFileSync;
const exec = (command, args) => {
@@ -18,12 +17,7 @@ const execGitCmd = args =>
module.exports = {
getStagedFiles: fileExtensionFilter => {
- const gitOptions = [
- 'diff',
- '--name-only',
- '--cached',
- '--diff-filter=ACMRTUB',
- ];
+ const gitOptions = ['diff', '--name-only', '--cached', '--diff-filter=ACMRTUB'];
if (fileExtensionFilter) gitOptions.push(...fileExtensionFilter);
return execGitCmd(gitOptions);
},
diff --git a/scripts/frontend/prettier.js b/scripts/frontend/prettier.js
index 863572bf64d..39de77bc333 100644
--- a/scripts/frontend/prettier.js
+++ b/scripts/frontend/prettier.js
@@ -1,7 +1,8 @@
-/* eslint import/no-commonjs: "off", import/no-extraneous-dependencies: "off", no-console: "off" */
const glob = require('glob');
const prettier = require('prettier');
const fs = require('fs');
+const path = require('path');
+const prettierIgnore = require('ignore')();
const getStagedFiles = require('./frontend_script_utils').getStagedFiles;
@@ -11,6 +12,10 @@ const allFiles = mode === 'check-all' || mode === 'save-all';
const config = {
patterns: ['**/*.js', '**/*.vue', '**/*.scss'],
+ /*
+ * The ignore patterns below are just to reduce search time with glob, as it includes the
+ * folders with the most ignored assets, the actual `.prettierignore` will be used later on
+ */
ignore: ['**/node_modules/**', '**/vendor/**', '**/public/**'],
parsers: {
js: 'babylon',
@@ -18,13 +23,25 @@ const config = {
scss: 'css',
},
};
+
+/*
+ * Unfortunately the prettier API does not expose support for `.prettierignore` files, they however
+ * use the ignore package, so we do the same. We simply cannot use the glob package, because
+ * gitignore style is not compatible with globs ignore style.
+ */
+prettierIgnore.add(
+ fs
+ .readFileSync(path.join(__dirname, '../../', '.prettierignore'))
+ .toString()
+ .trim()
+ .split(/\r?\n/)
+);
+
const availableExtensions = Object.keys(config.parsers);
console.log(`Loading ${allFiles ? 'All' : 'Staged'} Files ...`);
-const stagedFiles = allFiles
- ? null
- : getStagedFiles(availableExtensions.map(ext => `*.${ext}`));
+const stagedFiles = allFiles ? null : getStagedFiles(availableExtensions.map(ext => `*.${ext}`));
if (stagedFiles) {
if (!stagedFiles.length || (stagedFiles.length === 1 && !stagedFiles[0])) {
@@ -41,17 +58,14 @@ let files;
if (allFiles) {
const ignore = config.ignore;
const patterns = config.patterns;
- const globPattern =
- patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`;
- files = glob
- .sync(globPattern, { ignore })
- .filter(f => allFiles || stagedFiles.includes(f));
+ const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`;
+ files = glob.sync(globPattern, { ignore }).filter(f => allFiles || stagedFiles.includes(f));
} else {
- files = stagedFiles.filter(f =>
- availableExtensions.includes(f.split('.').pop()),
- );
+ files = stagedFiles.filter(f => availableExtensions.includes(f.split('.').pop()));
}
+files = prettierIgnore.filter(files);
+
if (!files.length) {
console.log('No Files found to process with Prettier');
return;
@@ -81,7 +95,7 @@ prettier
} else if (!prettier.check(input, options)) {
if (!didWarn) {
console.log(
- '\n===============================\nGitLab uses Prettier to format all JavaScript code.\nPlease format each file listed below or run "yarn prettier-staged-save"\n===============================\n',
+ '\n===============================\nGitLab uses Prettier to format all JavaScript code.\nPlease format each file listed below or run "yarn prettier-staged-save"\n===============================\n'
);
didWarn = true;
}
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index e5242fee32b..178b209aacf 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -3,7 +3,7 @@
cd "$(dirname "$0")/.."
# Use long options (e.g. --header instead of -H) for curl examples in documentation.
-echo 'Checking for curl short options...'
+echo '=> Checking for cURL short options...'
grep --extended-regexp --recursive --color=auto 'curl (.+ )?-[^- ].*' doc/ >/dev/null 2>&1
if [ $? == 0 ]
then
@@ -15,7 +15,7 @@ fi
# Ensure that the CHANGELOG.md does not contain duplicate versions
DUPLICATE_CHANGELOG_VERSIONS=$(grep --extended-regexp '^## .+' CHANGELOG.md | sed -E 's| \(.+\)||' | sort -r | uniq -d)
-echo 'Checking for CHANGELOG.md duplicate entries...'
+echo '=> Checking for CHANGELOG.md duplicate entries...'
if [ "${DUPLICATE_CHANGELOG_VERSIONS}" != "" ]
then
echo '✖ ERROR: Duplicate versions in CHANGELOG.md:' >&2
@@ -25,7 +25,7 @@ fi
# Make sure no files in doc/ are executable
EXEC_PERM_COUNT=$(find doc/ app/ -type f -perm 755 | wc -l)
-echo 'Checking for executable permissions...'
+echo '=> Checking for executable permissions...'
if [ "${EXEC_PERM_COUNT}" -ne 0 ]
then
echo '✖ ERROR: Executable permissions should not be used in documentation! Use `chmod 644` to the files in question:' >&2
@@ -33,5 +33,33 @@ then
exit 1
fi
+# Do not use 'README.md', instead use 'index.md'
+# Number of 'README.md's as of 2018-03-26
+NUMBER_READMES_CE=42
+NUMBER_READMES_EE=46
+FIND_READMES=$(find doc/ -name "README.md" | wc -l)
+echo '=> Checking for new README.md files...'
+if [ "${CI_PROJECT_NAME}" == 'gitlab-ce' ]
+then
+ if [ ${FIND_READMES} -ne ${NUMBER_READMES_CE} ]
+ then
+ echo
+ echo ' ✖ ERROR: New README.md file(s) detected, prefer index.md over README.md.' >&2
+ echo ' https://docs.gitlab.com/ee/development/writing_documentation.html#location-and-naming-documents'
+ echo
+ exit 1
+ fi
+elif [ "${CI_PROJECT_NAME}" == 'gitlab-ee' ]
+then
+ if [ ${FIND_READMES} -ne $NUMBER_READMES_EE ]
+ then
+ echo
+ echo ' ✖ ERROR: New README.md file(s) detected, prefer index.md over README.md.' >&2
+ echo ' https://docs.gitlab.com/ee/development/writing_documentation.html#location-and-naming-documents'
+ echo
+ exit 1
+ fi
+fi
+
echo "✔ Linting passed"
exit 0
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb
index d5a3c250f31..cc200b9fed9 100644
--- a/spec/controllers/admin/projects_controller_spec.rb
+++ b/spec/controllers/admin/projects_controller_spec.rb
@@ -31,5 +31,15 @@ describe Admin::ProjectsController do
expect(response.body).not_to match(pending_delete_project.name)
expect(response.body).to match(project.name)
end
+
+ it 'does not have N+1 queries', :use_clean_rails_memory_store_caching, :request_store do
+ get :index
+
+ control_count = ActiveRecord::QueryRecorder.new { get :index }.count
+
+ create(:project)
+
+ expect { get :index }.not_to exceed_query_limit(control_count)
+ end
end
end
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
new file mode 100644
index 00000000000..f4c99ea4064
--- /dev/null
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe SendFileUpload do
+ let(:uploader_class) do
+ Class.new(GitlabUploader) do
+ include ObjectStorage::Concern
+
+ storage_options Gitlab.config.uploads
+
+ private
+
+ # user/:id
+ def dynamic_segment
+ File.join(model.class.to_s.underscore, model.id.to_s)
+ end
+ end
+ end
+
+ let(:controller_class) do
+ Class.new do
+ include SendFileUpload
+ end
+ end
+
+ let(:object) { build_stubbed(:user) }
+ let(:uploader) { uploader_class.new(object, :file) }
+
+ describe '#send_upload' do
+ let(:controller) { controller_class.new }
+ let(:temp_file) { Tempfile.new('test') }
+
+ subject { controller.send_upload(uploader) }
+
+ before do
+ FileUtils.touch(temp_file)
+ end
+
+ after do
+ FileUtils.rm_f(temp_file)
+ end
+
+ context 'when local file is used' do
+ before do
+ uploader.store!(temp_file)
+ end
+
+ it 'sends a file' do
+ expect(controller).to receive(:send_file).with(uploader.path, anything)
+
+ subject
+ end
+ end
+
+ context 'when remote file is used' do
+ before do
+ stub_uploads_object_storage(uploader: uploader_class)
+ uploader.object_store = ObjectStorage::Store::REMOTE
+ uploader.store!(temp_file)
+ end
+
+ context 'and proxying is enabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { true }
+ end
+
+ it 'sends a file' do
+ headers = double
+ expect(headers).to receive(:store).with(Gitlab::Workhorse::SEND_DATA_HEADER, /^send-url:/)
+ expect(controller).to receive(:headers) { headers }
+ expect(controller).to receive(:head).with(:ok)
+
+ subject
+ end
+ end
+
+ context 'and proxying is disabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { false }
+ end
+
+ it 'sends a file' do
+ expect(controller).to receive(:redirect_to).with(/#{uploader.path}/)
+
+ subject
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index c639ad32ec6..5f0e8c5eca9 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -3,72 +3,125 @@ require 'spec_helper'
describe OmniauthCallbacksController do
include LoginHelpers
- let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: provider) }
- let(:provider) { :github }
+ let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
before do
- mock_auth_hash(provider.to_s, 'my-uid', user.email)
+ mock_auth_hash(provider.to_s, extern_uid, user.email)
stub_omniauth_provider(provider, context: request)
end
- it 'allows sign in' do
- post provider
+ context 'when the user is on the last sign in attempt' do
+ let(:extern_uid) { 'my-uid' }
- expect(request.env['warden']).to be_authenticated
- end
+ before do
+ user.update(failed_attempts: User.maximum_attempts.pred)
+ subject.response = ActionDispatch::Response.new
+ end
- shared_context 'sign_up' do
- let(:user) { double(email: 'new@example.com') }
+ context 'when using a form based provider' do
+ let(:provider) { :ldap }
- before do
- stub_omniauth_setting(block_auto_created_users: false)
+ it 'locks the user when sign in fails' do
+ allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
+ request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
+
+ subject.send(:failure)
+
+ expect(user.reload).to be_access_locked
+ end
end
- end
- context 'sign up' do
- include_context 'sign_up'
+ context 'when using a button based provider' do
+ let(:provider) { :github }
- it 'is allowed' do
- post provider
+ it 'does not lock the user when sign in fails' do
+ request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil)
- expect(request.env['warden']).to be_authenticated
+ subject.send(:failure)
+
+ expect(user.reload).not_to be_access_locked
+ end
end
end
- context 'when OAuth is disabled' do
- before do
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- settings = Gitlab::CurrentSettings.current_application_settings
- settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
- end
+ context 'strategies' do
+ context 'github' do
+ let(:extern_uid) { 'my-uid' }
+ let(:provider) { :github }
- it 'prevents login via POST' do
- post provider
+ it 'allows sign in' do
+ post provider
- expect(request.env['warden']).not_to be_authenticated
- end
+ expect(request.env['warden']).to be_authenticated
+ end
- it 'shows warning when attempting login' do
- post provider
+ shared_context 'sign_up' do
+ let(:user) { double(email: 'new@example.com') }
- expect(response).to redirect_to new_user_session_path
- expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
- end
+ before do
+ stub_omniauth_setting(block_auto_created_users: false)
+ end
+ end
+
+ context 'sign up' do
+ include_context 'sign_up'
+
+ it 'is allowed' do
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ end
+ end
+
+ context 'when OAuth is disabled' do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ settings = Gitlab::CurrentSettings.current_application_settings
+ settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
+ end
+
+ it 'prevents login via POST' do
+ post provider
+
+ expect(request.env['warden']).not_to be_authenticated
+ end
- it 'allows linking the disabled provider' do
- user.identities.destroy_all
- sign_in(user)
+ it 'shows warning when attempting login' do
+ post provider
- expect { post provider }.to change { user.reload.identities.count }.by(1)
+ expect(response).to redirect_to new_user_session_path
+ expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
+ end
+
+ it 'allows linking the disabled provider' do
+ user.identities.destroy_all
+ sign_in(user)
+
+ expect { post provider }.to change { user.reload.identities.count }.by(1)
+ end
+
+ context 'sign up' do
+ include_context 'sign_up'
+
+ it 'is prevented' do
+ post provider
+
+ expect(request.env['warden']).not_to be_authenticated
+ end
+ end
+ end
end
- context 'sign up' do
- include_context 'sign_up'
+ context 'auth0' do
+ let(:extern_uid) { '' }
+ let(:provider) { :auth0 }
- it 'is prevented' do
- post provider
+ it 'does not allow sign in without extern_uid' do
+ post 'auth0'
expect(request.env['warden']).not_to be_authenticated
+ expect(response.status).to eq(302)
+ expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
end
end
end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 25a2e13fe1a..4ea6f869aa3 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -145,9 +145,23 @@ describe Projects::ArtifactsController do
context 'when using local file storage' do
it_behaves_like 'a valid file' do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+ let(:store) { ObjectStorage::Store::LOCAL }
let(:archive_path) { JobArtifactUploader.root }
end
end
+
+ context 'when using remote file storage' do
+ before do
+ stub_artifacts_object_storage
+ end
+
+ it_behaves_like 'a valid file' do
+ let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
+ let!(:job) { create(:ci_build, :success, pipeline: pipeline) }
+ let(:store) { ObjectStorage::Store::REMOTE }
+ let(:archive_path) { 'https://' }
+ end
+ end
end
end
diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb
new file mode 100644
index 00000000000..1249a5528a9
--- /dev/null
+++ b/spec/controllers/projects/ci/lints_controller_spec.rb
@@ -0,0 +1,123 @@
+require 'spec_helper'
+
+describe Projects::Ci::LintsController do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #show' do
+ context 'with enough privileges' do
+ before do
+ project.add_developer(user)
+
+ get :show, namespace_id: project.namespace, project_id: project
+ end
+
+ it 'should be success' do
+ expect(response).to be_success
+ end
+
+ it 'should render show page' do
+ expect(response).to render_template :show
+ end
+
+ it 'should retrieve project' do
+ expect(assigns(:project)).to eq(project)
+ end
+ end
+
+ context 'without enough privileges' do
+ before do
+ project.add_guest(user)
+
+ get :show, namespace_id: project.namespace, project_id: project
+ end
+
+ it 'should respond with 404' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST #create' do
+ let(:remote_file_path) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
+
+ let(:remote_file_content) do
+ <<~HEREDOC
+ before_script:
+ - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
+ - ruby -v
+ - which ruby
+ - gem install bundler --no-ri --no-rdoc
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
+ HEREDOC
+ end
+
+ let(:content) do
+ <<~HEREDOC
+ include:
+ - #{remote_file_path}
+
+ rubocop:
+ script:
+ - bundle exec rubocop
+ HEREDOC
+ end
+
+ context 'with a valid gitlab-ci.yml' do
+ before do
+ WebMock.stub_request(:get, remote_file_path).to_return(body: remote_file_content)
+ project.add_developer(user)
+
+ post :create, namespace_id: project.namespace, project_id: project, content: content
+ end
+
+ it 'should be success' do
+ expect(response).to be_success
+ end
+
+ it 'render show page' do
+ expect(response).to render_template :show
+ end
+
+ it 'should retrieve project' do
+ expect(assigns(:project)).to eq(project)
+ end
+ end
+
+ context 'with an invalid gitlab-ci.yml' do
+ let(:content) do
+ <<~HEREDOC
+ rubocop:
+ scriptt:
+ - bundle exec rubocop
+ HEREDOC
+ end
+
+ before do
+ project.add_developer(user)
+
+ post :create, namespace_id: project.namespace, project_id: project, content: content
+ end
+
+ it 'should assign errors' do
+ expect(assigns[:error]).to eq('jobs:rubocop config contains unknown keys: scriptt')
+ end
+ end
+
+ context 'without enough privileges' do
+ before do
+ project.add_guest(user)
+
+ post :create, namespace_id: project.namespace, project_id: project, content: content
+ end
+
+ it 'should respond with 404' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 15ce418d0d6..82b20e12850 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -18,7 +18,7 @@ describe Projects::ClustersController do
context 'when project has one or more clusters' do
let(:project) { create(:project) }
let!(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- let!(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
+ let!(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, :production_environment, projects: [project]) }
it 'lists available clusters' do
go
@@ -32,7 +32,7 @@ describe Projects::ClustersController do
before do
allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
- create_list(:cluster, 2, :provided_by_gcp, projects: [project])
+ create_list(:cluster, 2, :provided_by_gcp, :production_environment, projects: [project])
get :index, namespace_id: project.namespace, project_id: project, page: last_page
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index f3e303bb0fe..31046c202e6 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -1,7 +1,9 @@
+# coding: utf-8
require 'spec_helper'
describe Projects::JobsController do
include ApiHelpers
+ include HttpIOHelpers
let(:project) { create(:project, :public) }
let(:pipeline) { create(:ci_pipeline, project: project) }
@@ -203,6 +205,41 @@ describe Projects::JobsController do
end
end
+ context 'when trace artifact is in ObjectStorage' do
+ let!(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
+
+ before do
+ allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
+ allow_any_instance_of(JobArtifactUploader).to receive(:url) { remote_trace_url }
+ allow_any_instance_of(JobArtifactUploader).to receive(:size) { remote_trace_size }
+ end
+
+ context 'when there are no network issues' do
+ before do
+ stub_remote_trace_206
+
+ get_trace
+ end
+
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['html']).to eq(job.trace.html)
+ end
+ end
+
+ context 'when there is a network issue' do
+ before do
+ stub_remote_trace_500
+ end
+
+ it 'returns a trace' do
+ expect { get_trace }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError)
+ end
+ end
+ end
+
def get_trace
get :trace, namespace_id: project.namespace,
project_id: project,
@@ -446,14 +483,18 @@ describe Projects::JobsController do
end
describe 'GET raw' do
- before do
- get_raw
+ subject do
+ post :raw, namespace_id: project.namespace,
+ project_id: project,
+ id: job.id
end
context 'when job has a trace artifact' do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'returns a trace' do
+ response = subject
+
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain; charset=utf-8'
expect(response.body).to eq job.job_artifacts_trace.open.read
@@ -464,6 +505,8 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
it 'send a trace file' do
+ response = subject
+
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain; charset=utf-8'
expect(response.body).to eq 'BUILD TRACE'
@@ -474,14 +517,22 @@ describe Projects::JobsController do
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'returns not_found' do
+ response = subject
+
expect(response).to have_gitlab_http_status(:not_found)
end
end
- def get_raw
- post :raw, namespace_id: project.namespace,
- project_id: project,
- id: job.id
+ context 'when the trace artifact is in ObjectStorage' do
+ let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+ before do
+ allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
+ end
+
+ it 'redirect to the trace file url' do
+ expect(subject).to redirect_to(job.job_artifacts_trace.file.url)
+ end
end
end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 306094f7ffb..5b2614163ff 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -20,14 +20,23 @@ describe Projects::MilestonesController do
describe "#show" do
render_views
- def view_milestone
- get :show, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
+ def view_milestone(options = {})
+ params = { namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid }
+ get :show, params.merge(options)
end
it 'shows milestone page' do
view_milestone
expect(response).to have_gitlab_http_status(200)
+ expect(response.content_type).to eq 'text/html'
+ end
+
+ it 'returns milestone json' do
+ view_milestone format: :json
+
+ expect(response).to have_http_status(404)
+ expect(response.content_type).to eq 'application/json'
end
end
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 4705c50de7e..11f54eef531 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -65,4 +65,41 @@ describe Projects::PagesController do
end
end
end
+
+ describe 'PATCH update' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ project: { pages_https_only: false }
+ }
+ end
+
+ let(:update_service) { double(execute: { status: :success }) }
+
+ before do
+ allow(Projects::UpdateService).to receive(:new) { update_service }
+ end
+
+ it 'returns 302 status' do
+ patch :update, request_params
+
+ expect(response).to have_gitlab_http_status(:found)
+ end
+
+ it 'redirects back to the pages settings' do
+ patch :update, request_params
+
+ expect(response).to redirect_to(project_pages_path(project))
+ end
+
+ it 'calls the update service' do
+ expect(Projects::UpdateService)
+ .to receive(:new)
+ .with(project, user, request_params[:project])
+ .and_return(update_service)
+
+ patch :update, request_params
+ end
+ end
end
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
index 83a3799e883..d4058a5c515 100644
--- a/spec/controllers/projects/pages_domains_controller_spec.rb
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::PagesDomainsController do
end
let(:pages_domain_params) do
- build(:pages_domain, :with_certificate, :with_key, domain: 'my.otherdomain.com').slice(:key, :certificate, :domain)
+ build(:pages_domain, domain: 'my.otherdomain.com').slice(:key, :certificate, :domain)
end
before do
@@ -68,7 +68,7 @@ describe Projects::PagesDomainsController do
end
let(:pages_domain_params) do
- attributes_for(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate)
+ attributes_for(:pages_domain).slice(:key, :certificate)
end
let(:params) do
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 966ffdf6996..3506305f755 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -80,7 +80,7 @@ describe Projects::PipelineSchedulesController do
context 'when variables_attributes has one variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', value: 'AAA123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' }]
})
end
@@ -101,7 +101,8 @@ describe Projects::PipelineSchedulesController do
context 'when variables_attributes has two variables and duplicated' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', value: 'AAA123' }, { key: 'AAA', value: 'BBB123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' },
+ { key: 'AAA', secret_value: 'BBB123' }]
})
end
@@ -152,7 +153,7 @@ describe Projects::PipelineSchedulesController do
context 'when params include one variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', value: 'AAA123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' }]
})
end
@@ -169,7 +170,8 @@ describe Projects::PipelineSchedulesController do
context 'when params include two duplicated variables' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', value: 'AAA123' }, { key: 'AAA', value: 'BBB123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' },
+ { key: 'AAA', secret_value: 'BBB123' }]
})
end
@@ -194,7 +196,7 @@ describe Projects::PipelineSchedulesController do
context 'when adds a new variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', value: 'AAA123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' }]
})
end
@@ -209,7 +211,7 @@ describe Projects::PipelineSchedulesController do
context 'when adds a new duplicated variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'CCC', value: 'AAA123' }]
+ variables_attributes: [{ key: 'CCC', secret_value: 'AAA123' }]
})
end
@@ -224,7 +226,7 @@ describe Projects::PipelineSchedulesController do
context 'when updates a variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ id: pipeline_schedule_variable.id, value: 'new_value' }]
+ variables_attributes: [{ id: pipeline_schedule_variable.id, secret_value: 'new_value' }]
})
end
@@ -252,7 +254,7 @@ describe Projects::PipelineSchedulesController do
let(:schedule) do
basic_param.merge({
variables_attributes: [{ id: pipeline_schedule_variable.id, _destroy: true },
- { key: 'CCC', value: 'CCC123' }]
+ { key: 'CCC', secret_value: 'CCC123' }]
})
end
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb
index 1cc488bef32..913b9bd804a 100644
--- a/spec/controllers/projects/pipelines_settings_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb
@@ -47,10 +47,32 @@ describe Projects::PipelinesSettingsController do
expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(true)
end
- it 'queues a CreatePipelineWorker' do
- expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
+ context 'when the project repository is empty' do
+ it 'sets a warning flash' do
+ expect(subject).to set_flash[:warning]
+ end
- subject
+ it 'does not queue a CreatePipelineWorker' do
+ expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
+
+ subject
+ end
+ end
+
+ context 'when the project repository is not empty' do
+ let(:project) { create(:project, :repository) }
+
+ it 'sets a success flash' do
+ allow(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
+
+ expect(subject).to set_flash[:success]
+ end
+
+ it 'queues a CreatePipelineWorker' do
+ expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
+
+ subject
+ end
end
end
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index 80be135b5d8..096e29bc39f 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -1,6 +1,16 @@
require('spec_helper')
describe Projects::ProtectedBranchesController do
+ let(:project) { create(:project, :repository) }
+ let(:protected_branch) { create(:protected_branch, project: project) }
+ let(:project_params) { { namespace_id: project.namespace.to_param, project_id: project } }
+ let(:base_params) { project_params.merge(id: protected_branch.id) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ end
+
describe "GET #index" do
let(:project) { create(:project_empty_repo, :public) }
@@ -8,4 +18,91 @@ describe Projects::ProtectedBranchesController do
get(:index, namespace_id: project.namespace.to_param, project_id: project)
end
end
+
+ describe "POST #create" do
+ let(:master_access_level) { [{ access_level: Gitlab::Access::MASTER }] }
+ let(:access_level_params) do
+ { merge_access_levels_attributes: master_access_level,
+ push_access_levels_attributes: master_access_level }
+ end
+ let(:create_params) { attributes_for(:protected_branch).merge(access_level_params) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'creates the protected branch rule' do
+ expect do
+ post(:create, project_params.merge(protected_branch: create_params))
+ end.to change(ProtectedBranch, :count).by(1)
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ allow(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents creation of the protected branch rule" do
+ post(:create, project_params.merge(protected_branch: create_params))
+
+ expect(ProtectedBranch.count).to eq 0
+ end
+ end
+ end
+
+ describe "PUT #update" do
+ let(:update_params) { { name: 'new_name' } }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'updates the protected branch rule' do
+ put(:update, base_params.merge(protected_branch: update_params))
+
+ expect(protected_branch.reload.name).to eq('new_name')
+ expect(json_response["name"]).to eq('new_name')
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ allow(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents update of the protected branch rule" do
+ old_name = protected_branch.name
+
+ put(:update, base_params.merge(protected_branch: update_params))
+
+ expect(protected_branch.reload.name).to eq(old_name)
+ end
+ end
+ end
+
+ describe "DELETE #destroy" do
+ before do
+ sign_in(user)
+ end
+
+ it "deletes the protected branch rule" do
+ delete(:destroy, base_params)
+
+ expect { ProtectedBranch.find(protected_branch.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ allow(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ delete(:destroy, base_params)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index b7df42168e0..08e2ccf893a 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -8,10 +8,7 @@ describe Projects::RawController do
let(:id) { 'master/README.md' }
it 'delivers ASCII file' do
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ get_show(public_project, id)
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
@@ -25,10 +22,7 @@ describe Projects::RawController do
let(:id) { 'master/files/images/6049019_460s.jpg' }
it 'sets image content type header' do
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ get_show(public_project, id)
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('image/jpeg')
@@ -54,21 +48,40 @@ describe Projects::RawController do
it 'serves the file' do
expect(controller).to receive(:send_file).with("#{LfsObjectUploader.root}/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment')
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ get_show(public_project, id)
expect(response).to have_gitlab_http_status(200)
end
+
+ context 'and lfs uses object storage' do
+ before do
+ lfs_object.file = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png")
+ lfs_object.save!
+ stub_lfs_object_storage
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
+
+ it 'responds with redirect to file' do
+ get_show(public_project, id)
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(response.location).to include(lfs_object.reload.file.path)
+ end
+
+ it 'sets content disposition' do
+ get_show(public_project, id)
+
+ file_uri = URI.parse(response.location)
+ params = CGI.parse(file_uri.query)
+
+ expect(params["response-content-disposition"].first).to eq 'attachment;filename="lfs_object.iso"'
+ end
+ end
end
context 'when project does not have access' do
it 'does not serve the file' do
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ get_show(public_project, id)
expect(response).to have_gitlab_http_status(404)
end
@@ -81,10 +94,7 @@ describe Projects::RawController do
end
it 'delivers ASCII file' do
- get(:show,
- namespace_id: public_project.namespace.to_param,
- project_id: public_project,
- id: id)
+ get_show(public_project, id)
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
@@ -95,4 +105,10 @@ describe Projects::RawController do
end
end
end
+
+ def get_show(project, id)
+ get(:show, namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: id)
+ end
end
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index b32eb39b1fb..7688538a468 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -90,6 +90,30 @@ describe RootController do
end
end
+ context 'who has customized their dashboard setting for assigned issues' do
+ before do
+ user.dashboard = 'issues'
+ end
+
+ it 'redirects to their assigned issues' do
+ get :index
+
+ expect(response).to redirect_to issues_dashboard_path(assignee_id: user.id)
+ end
+ end
+
+ context 'who has customized their dashboard setting for assigned merge requests' do
+ before do
+ user.dashboard = 'merge_requests'
+ end
+
+ it 'redirects to their assigned merge requests' do
+ get :index
+
+ expect(response).to redirect_to merge_requests_dashboard_path(assignee_id: user.id)
+ end
+ end
+
context 'who uses the default dashboard setting' do
it 'renders the default dashboard' do
get :index
diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb
index 5f9c57c0c8d..18c7453bd1b 100644
--- a/spec/factories/appearances.rb
+++ b/spec/factories/appearances.rb
@@ -2,8 +2,21 @@
FactoryBot.define do
factory :appearance do
- title "MepMep"
- description "This is my Community Edition instance"
+ title "GitLab Community Edition"
+ description "Open source software to collaborate on code"
new_project_guidelines "Custom project guidelines"
end
+
+ trait :with_logo do
+ logo { fixture_file_upload('spec/fixtures/dk.png') }
+ end
+
+ trait :with_header_logo do
+ header_logo { fixture_file_upload('spec/fixtures/dk.png') }
+ end
+
+ trait :with_logos do
+ with_logo
+ with_header_logo
+ end
end
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 8544d54ccaa..3d3287d8168 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -5,6 +5,10 @@ FactoryBot.define do
job factory: :ci_build
file_type :archive
+ trait :remote_store do
+ file_store JobArtifactUploader::Store::REMOTE
+ end
+
after :build do |artifact|
artifact.project ||= artifact.job.project
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 20d5580f0c2..98566f907f9 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -32,5 +32,9 @@ FactoryBot.define do
trait :disabled do
enabled false
end
+
+ trait :production_environment do
+ sequence(:environment_scope) { |n| "production#{n}/*" }
+ end
end
end
diff --git a/spec/factories/internal_ids.rb b/spec/factories/internal_ids.rb
new file mode 100644
index 00000000000..fbde07a391a
--- /dev/null
+++ b/spec/factories/internal_ids.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :internal_id do
+ project
+ usage :issues
+ last_value { project.issues.maximum(:iid) || 0 }
+ end
+end
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
index caaed4d5246..eaf3a4ed497 100644
--- a/spec/factories/lfs_objects.rb
+++ b/spec/factories/lfs_objects.rb
@@ -15,4 +15,8 @@ FactoryBot.define do
trait :correct_oid do
oid 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'
end
+
+ trait :object_storage do
+ file_store { LfsObjectUploader::Store::REMOTE }
+ end
end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index 35b44e1c52e..20671da016e 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -4,25 +4,7 @@ FactoryBot.define do
verified_at { Time.now }
enabled_until { 1.week.from_now }
- trait :disabled do
- verified_at nil
- enabled_until nil
- end
-
- trait :unverified do
- verified_at nil
- end
-
- trait :reverify do
- enabled_until { 1.hour.from_now }
- end
-
- trait :expired do
- enabled_until { 1.hour.ago }
- end
-
- trait :with_certificate do
- certificate '-----BEGIN CERTIFICATE-----
+ certificate '-----BEGIN CERTIFICATE-----
MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
@@ -36,10 +18,8 @@ joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
YHi2yesCrOvVXt+lgPTd
-----END CERTIFICATE-----'
- end
- trait :with_key do
- key '-----BEGIN PRIVATE KEY-----
+ key '-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
@@ -55,6 +35,30 @@ EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
nNp/xedE1YxutQ==
-----END PRIVATE KEY-----'
+
+ trait :disabled do
+ verified_at nil
+ enabled_until nil
+ end
+
+ trait :unverified do
+ verified_at nil
+ end
+
+ trait :reverify do
+ enabled_until { 1.hour.from_now }
+ end
+
+ trait :expired do
+ enabled_until { 1.hour.ago }
+ end
+
+ trait :without_certificate do
+ certificate nil
+ end
+
+ trait :without_key do
+ key nil
end
trait :with_missing_chain do
diff --git a/spec/factories/redirect_routes.rb b/spec/factories/redirect_routes.rb
index c29c81c5df9..774232d0b34 100644
--- a/spec/factories/redirect_routes.rb
+++ b/spec/factories/redirect_routes.rb
@@ -2,14 +2,5 @@ FactoryBot.define do
factory :redirect_route do
sequence(:path) { |n| "redirect#{n}" }
source factory: :group
- permanent false
-
- trait :permanent do
- permanent true
- end
-
- trait :temporary do
- permanent false
- end
end
end
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index ff3a2a76acc..b45f6f30e40 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -5,6 +5,7 @@ FactoryBot.define do
uploader "AvatarUploader"
mount_point :avatar
secret nil
+ store ObjectStorage::Store::LOCAL
# we should build a mount agnostic upload by default
transient do
@@ -27,6 +28,10 @@ FactoryBot.define do
secret SecureRandom.hex
end
+ trait :object_storage do
+ store ObjectStorage::Store::REMOTE
+ end
+
trait :namespace_upload do
model { build(:group) }
path { File.join(secret, filename) }
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index 9ea3cfa72c6..9946cc77d1d 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -55,14 +55,19 @@ feature 'Admin disables Git access protocol' do
end
def disable_http_protocol
- visit admin_application_settings_path
- find('#application_setting_enabled_git_access_protocol').find(:xpath, 'option[2]').select_option
- click_on 'Save'
+ switch_git_protocol(2)
end
def disable_ssh_protocol
+ switch_git_protocol(3)
+ end
+
+ def switch_git_protocol(value)
visit admin_application_settings_path
- find('#application_setting_enabled_git_access_protocol').find(:xpath, 'option[3]').select_option
- click_on 'Save'
+
+ page.within('.as-visibility-access') do
+ find('#application_setting_enabled_git_access_protocol').find(:xpath, "option[#{value}]").select_option
+ click_on 'Save'
+ end
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 39b213988f0..c89bc54cad4 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -10,18 +10,21 @@ feature 'Admin updates settings' do
end
scenario 'Change visibility settings' do
- choose "application_setting_default_project_visibility_20"
- click_button 'Save'
+ page.within('.as-visibility-access') do
+ choose "application_setting_default_project_visibility_20"
+ click_button 'Save changes'
+ end
expect(page).to have_content "Application settings saved successfully"
end
scenario 'Uncheck all restricted visibility levels' do
- find('#application_setting_visibility_level_0').set(false)
- find('#application_setting_visibility_level_10').set(false)
- find('#application_setting_visibility_level_20').set(false)
-
- click_button 'Save'
+ page.within('.as-visibility-access') do
+ find('#application_setting_visibility_level_0').set(false)
+ find('#application_setting_visibility_level_10').set(false)
+ find('#application_setting_visibility_level_20').set(false)
+ click_button 'Save changes'
+ end
expect(page).to have_content "Application settings saved successfully"
expect(find('#application_setting_visibility_level_0')).not_to be_checked
@@ -29,34 +32,94 @@ feature 'Admin updates settings' do
expect(find('#application_setting_visibility_level_20')).not_to be_checked
end
- scenario 'Change application settings' do
- uncheck 'Gravatar enabled'
- fill_in 'Home page URL', with: 'https://about.gitlab.com/'
- fill_in 'Help page text', with: 'Example text'
- check 'Hide marketing-related entries from help'
- fill_in 'Support page URL', with: 'http://example.com/help'
- uncheck 'Project export enabled'
- click_button 'Save'
+ scenario 'Change Visibility and Access Controls' do
+ page.within('.as-visibility-access') do
+ uncheck 'Project export enabled'
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.project_export_enabled).to be_falsey
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change Account and Limit Settings' do
+ page.within('.as-account-limit') do
+ uncheck 'Gravatar enabled'
+ click_button 'Save changes'
+ end
expect(Gitlab::CurrentSettings.gravatar_enabled).to be_falsey
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change Sign-in restrictions' do
+ page.within('.as-signin') do
+ fill_in 'Home page URL', with: 'https://about.gitlab.com/'
+ click_button 'Save changes'
+ end
+
expect(Gitlab::CurrentSettings.home_page_url).to eq "https://about.gitlab.com/"
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change Help page' do
+ page.within('.as-help-page') do
+ fill_in 'Help page text', with: 'Example text'
+ check 'Hide marketing-related entries from help'
+ fill_in 'Support page URL', with: 'http://example.com/help'
+ click_button 'Save changes'
+ end
+
expect(Gitlab::CurrentSettings.help_page_text).to eq "Example text"
expect(Gitlab::CurrentSettings.help_page_hide_commercial_content).to be_truthy
expect(Gitlab::CurrentSettings.help_page_support_url).to eq "http://example.com/help"
- expect(Gitlab::CurrentSettings.project_export_enabled).to be_falsey
expect(page).to have_content "Application settings saved successfully"
end
- scenario 'Change AutoDevOps settings' do
- check 'Enabled Auto DevOps (Beta) for projects by default'
- fill_in 'Auto devops domain', with: 'domain.com'
- click_button 'Save'
+ scenario 'Change Pages settings' do
+ page.within('.as-pages') do
+ fill_in 'Maximum size of pages (MB)', with: 15
+ check 'Require users to prove ownership of custom domains'
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.max_pages_size).to eq 15
+ expect(Gitlab::CurrentSettings.pages_domain_verification_enabled?).to be_truthy
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change CI/CD settings' do
+ page.within('.as-ci-cd') do
+ check 'Enabled Auto DevOps (Beta) for projects by default'
+ fill_in 'Auto devops domain', with: 'domain.com'
+ click_button 'Save changes'
+ end
expect(Gitlab::CurrentSettings.auto_devops_enabled?).to be true
expect(Gitlab::CurrentSettings.auto_devops_domain).to eq('domain.com')
expect(page).to have_content "Application settings saved successfully"
end
+ scenario 'Change Influx settings' do
+ page.within('.as-influx') do
+ check 'Enable InfluxDB Metrics'
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.metrics_enabled?).to be true
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
+ scenario 'Change Prometheus settings' do
+ page.within('.as-prometheus') do
+ check 'Enable Prometheus Metrics'
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.prometheus_metrics_enabled?).to be true
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
scenario 'Change Slack Notifications Service template settings' do
first(:link, 'Service Templates').click
click_link 'Slack notifications'
@@ -83,18 +146,22 @@ feature 'Admin updates settings' do
context 'sign-in restrictions', :js do
it 'de-activates oauth sign-in source' do
- find('input#application_setting_enabled_oauth_sign_in_sources_[value=gitlab]').send_keys(:return)
+ page.within('.as-signin') do
+ find('input#application_setting_enabled_oauth_sign_in_sources_[value=gitlab]').send_keys(:return)
- expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active')
+ expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active')
+ end
end
end
scenario 'Change Keys settings' do
- select 'Are forbidden', from: 'RSA SSH keys'
- select 'Are allowed', from: 'DSA SSH keys'
- select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
- select 'Are forbidden', from: 'ED25519 SSH keys'
- click_on 'Save'
+ page.within('.as-visibility-access') do
+ select 'Are forbidden', from: 'RSA SSH keys'
+ select 'Are allowed', from: 'DSA SSH keys'
+ select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
+ select 'Are forbidden', from: 'ED25519 SSH keys'
+ click_on 'Save changes'
+ end
forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE.to_s
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index f82ed6300cc..4d897f09b57 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -20,7 +20,7 @@ describe 'Copy as GFM', :js do
end
# The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
- # The handlers defined in app/assets/javascripts/copy_as_gfm.js consequently convert that same HTML to GFM.
+ # The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js consequently convert that same HTML to GFM.
# To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
# by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/projects/ci/lint_spec.rb
index 220b934154e..313950072e7 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/projects/ci/lint_spec.rb
@@ -1,10 +1,14 @@
require 'spec_helper'
describe 'CI Lint', :js do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
before do
- sign_in(create(:user))
+ project.add_developer(user)
+ sign_in(user)
- visit ci_lint_path
+ visit project_ci_lint_path(project)
find('#ci-editor')
execute_script("ace.edit('ci-editor').setValue(#{yaml_content.to_json});")
diff --git a/spec/features/projects/hook_logs/user_reads_log_spec.rb b/spec/features/projects/hook_logs/user_reads_log_spec.rb
new file mode 100644
index 00000000000..18e975fa653
--- /dev/null
+++ b/spec/features/projects/hook_logs/user_reads_log_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'Hook logs' do
+ given(:web_hook_log) { create(:web_hook_log, response_body: '<script>') }
+ given(:project) { web_hook_log.web_hook.project }
+ given(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+
+ sign_in(user)
+ end
+
+ scenario 'user reads log without getting XSS' do
+ visit(
+ project_hook_hook_log_path(
+ project, web_hook_log.web_hook, web_hook_log))
+
+ expect(page).to have_content('<script>')
+ end
+end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index 233d2e67b9d..bdd49f731c7 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -40,11 +40,6 @@ feature 'Pages' do
end
context 'when support for external domains is disabled' do
- before do
- allow(Gitlab.config.pages).to receive(:external_http).and_return(nil)
- allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
- end
-
it 'renders message that support is disabled' do
visit project_pages_path(project)
@@ -52,7 +47,9 @@ feature 'Pages' do
end
end
- context 'when pages are exposed on external HTTP address' do
+ context 'when pages are exposed on external HTTP address', :http_pages_enabled do
+ given(:project) { create(:project, pages_https_only: false) }
+
shared_examples 'adds new domain' do
it 'adds new domain' do
visit new_project_pages_domain_path(project)
@@ -64,11 +61,6 @@ feature 'Pages' do
end
end
- before do
- allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
- allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
- end
-
it 'allows to add new domain' do
visit project_pages_path(project)
@@ -80,13 +72,13 @@ feature 'Pages' do
context 'when project in group namespace' do
it_behaves_like 'adds new domain' do
let(:group) { create :group }
- let(:project) { create :project, namespace: group }
+ let(:project) { create(:project, namespace: group, pages_https_only: false) }
end
end
context 'when pages domain is added' do
before do
- project.pages_domains.create!(domain: 'my.test.domain.com')
+ create(:pages_domain, project: project, domain: 'my.test.domain.com')
visit new_project_pages_domain_path(project)
end
@@ -104,7 +96,7 @@ feature 'Pages' do
end
end
- context 'when pages are exposed on external HTTPS address' do
+ context 'when pages are exposed on external HTTPS address', :https_pages_enabled do
let(:certificate_pem) do
<<~PEM
-----BEGIN CERTIFICATE-----
@@ -145,11 +137,6 @@ feature 'Pages' do
KEY
end
- before do
- allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
- allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443'])
- end
-
it 'adds new domain with certificate' do
visit new_project_pages_domain_path(project)
@@ -163,7 +150,7 @@ feature 'Pages' do
describe 'updating the certificate for an existing domain' do
let!(:domain) do
- create(:pages_domain, :with_key, :with_certificate, project: project)
+ create(:pages_domain, project: project)
end
it 'allows the certificate to be updated' do
@@ -237,6 +224,70 @@ feature 'Pages' do
it_behaves_like 'no pages deployed'
end
+ describe 'HTTPS settings', :js, :https_pages_enabled do
+ background do
+ project.namespace.update(owner: user)
+
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ scenario 'tries to change the setting' do
+ visit project_pages_path(project)
+ expect(page).to have_content("Force domains with SSL certificates to use HTTPS")
+
+ uncheck :project_pages_https_only
+
+ click_button 'Save'
+
+ expect(page).to have_text('Your changes have been saved')
+ expect(page).not_to have_checked_field('project_pages_https_only')
+ end
+
+ context 'setting could not be updated' do
+ let(:service) { instance_double('Projects::UpdateService') }
+
+ before do
+ allow(Projects::UpdateService).to receive(:new).and_return(service)
+ allow(service).to receive(:execute).and_return(status: :error)
+ end
+
+ scenario 'tries to change the setting' do
+ visit project_pages_path(project)
+
+ uncheck :project_pages_https_only
+
+ click_button 'Save'
+
+ expect(page).to have_text('Something went wrong on our end')
+ end
+ end
+
+ context 'non-HTTPS domain exists' do
+ given(:project) { create(:project, pages_https_only: false) }
+
+ before do
+ create(:pages_domain, :without_key, :without_certificate, project: project)
+ end
+
+ scenario 'the setting is disabled' do
+ visit project_pages_path(project)
+
+ expect(page).to have_field(:project_pages_https_only, disabled: true)
+ expect(page).not_to have_button('Save')
+ end
+ end
+
+ context 'HTTPS pages are disabled', :https_pages_disabled do
+ scenario 'the setting is unavailable' do
+ visit project_pages_path(project)
+
+ expect(page).not_to have_field(:project_pages_https_only)
+ expect(page).not_to have_content('Force domains with SSL certificates to use HTTPS')
+ expect(page).not_to have_button('Save')
+ end
+ end
+ end
+
describe 'Remove page' do
context 'when user is the owner' do
let(:project) { create :project, :repository }
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index 65e24862d43..065d00d51d4 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -160,9 +160,9 @@ feature 'Pipeline Schedules', :js do
click_link 'New schedule'
fill_in_schedule_form
all('[name="schedule[variables_attributes][][key]"]')[0].set('AAA')
- all('[name="schedule[variables_attributes][][value]"]')[0].set('AAA123')
+ all('[name="schedule[variables_attributes][][secret_value]"]')[0].set('AAA123')
all('[name="schedule[variables_attributes][][key]"]')[1].set('BBB')
- all('[name="schedule[variables_attributes][][value]"]')[1].set('BBB123')
+ all('[name="schedule[variables_attributes][][secret_value]"]')[1].set('BBB123')
save_pipeline_schedule
end
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
new file mode 100644
index 00000000000..d96c7e655ba
--- /dev/null
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+feature 'Multi-file editor new directory', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+
+ click_link('Web IDE')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
+ end
+
+ it 'creates directory in current directory' do
+ find('.add-to-tree').click
+
+ click_link('New directory')
+
+ page.within('.modal') do
+ find('.form-control').set('folder name')
+
+ click_button('Create directory')
+ end
+
+ find('.add-to-tree').click
+
+ click_link('New file')
+
+ page.within('.modal-dialog') do
+ find('.form-control').set('file name')
+
+ click_button('Create file')
+ end
+
+ wait_for_requests
+
+ fill_in('commit-message', with: 'commit message ide')
+
+ click_button('Commit')
+
+ expect(page).to have_content('folder name')
+ end
+end
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
new file mode 100644
index 00000000000..a4cbd5cf766
--- /dev/null
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+feature 'Multi-file editor new file', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_path(project)
+
+ wait_for_requests
+
+ click_link('Web IDE')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
+ end
+
+ it 'creates file in current directory' do
+ find('.add-to-tree').click
+
+ click_link('New file')
+
+ page.within('.modal') do
+ find('.form-control').set('file name')
+
+ click_button('Create file')
+ end
+
+ wait_for_requests
+
+ fill_in('commit-message', with: 'commit message ide')
+
+ click_button('Commit')
+
+ expect(page).to have_content('file name')
+ end
+end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index c8a17871508..c4b3fb9d171 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -25,4 +25,18 @@ feature 'Projects tree' do
expect(page).to have_selector('.label-lfs', text: 'LFS')
end
end
+
+ context 'web IDE', :js do
+ before do
+ visit project_tree_path(project, File.join('master', 'bar'))
+
+ click_link 'Web IDE'
+
+ find('.ide-file-list')
+ end
+
+ it 'opens folder in IDE' do
+ expect(page).to have_selector('.is-open', text: 'bar')
+ end
+ end
end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
new file mode 100644
index 00000000000..8e53ae15700
--- /dev/null
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+feature 'Multi-file editor upload file', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') }
+ let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_tree_path(project, :master)
+
+ wait_for_requests
+
+ click_link('Web IDE')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
+ end
+
+ it 'uploads text file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', txt_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
+ expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
+ end
+
+ it 'uploads image file' do
+ find('.add-to-tree').click
+
+ # make the field visible so capybara can use it
+ execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
+ attach_file('file-upload', img_file)
+
+ find('.add-to-tree').click
+
+ expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
+ expect(page).not_to have_selector('.monaco-editor')
+ end
+end
diff --git a/spec/features/read_only_spec.rb b/spec/features/read_only_spec.rb
new file mode 100644
index 00000000000..8bfaf558466
--- /dev/null
+++ b/spec/features/read_only_spec.rb
@@ -0,0 +1,25 @@
+require 'rails_helper'
+
+describe 'read-only message' do
+ set(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'shows read-only banner when database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+
+ visit root_dashboard_path
+
+ expect(page).to have_content('You are on a read-only GitLab instance.')
+ end
+
+ it 'does not show read-only banner when database is able to read-write' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(false)
+
+ visit root_dashboard_path
+
+ expect(page).not_to have_content('You are on a read-only GitLab instance.')
+ end
+end
diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb
index 975c157bcf5..e069c2fddd1 100644
--- a/spec/features/user_can_display_performance_bar_spec.rb
+++ b/spec/features/user_can_display_performance_bar_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe 'User can display performance bar', :js do
shared_examples 'performance bar cannot be displayed' do
it 'does not show the performance bar by default' do
- expect(page).not_to have_css('#peek')
+ expect(page).not_to have_css('#js-peek')
end
context 'when user press `pb`' do
@@ -12,14 +12,14 @@ describe 'User can display performance bar', :js do
end
it 'does not show the performance bar by default' do
- expect(page).not_to have_css('#peek')
+ expect(page).not_to have_css('#js-peek')
end
end
end
shared_examples 'performance bar can be displayed' do
it 'does not show the performance bar by default' do
- expect(page).not_to have_css('#peek')
+ expect(page).not_to have_css('#js-peek')
end
context 'when user press `pb`' do
@@ -28,7 +28,7 @@ describe 'User can display performance bar', :js do
end
it 'shows the performance bar' do
- expect(page).to have_css('#peek')
+ expect(page).to have_css('#js-peek')
end
end
end
@@ -41,7 +41,7 @@ describe 'User can display performance bar', :js do
it 'shows the performance bar by default' do
refresh # Because we're stubbing Rails.env after the 1st visit to root_path
- expect(page).to have_css('#peek')
+ expect(page).to have_css('#js-peek')
end
end
diff --git a/spec/finders/clusters_finder_spec.rb b/spec/finders/clusters_finder_spec.rb
index c10efac2432..da529e0670f 100644
--- a/spec/finders/clusters_finder_spec.rb
+++ b/spec/finders/clusters_finder_spec.rb
@@ -6,7 +6,7 @@ describe ClustersFinder do
describe '#execute' do
let(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- let(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
+ let(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, :production_environment, projects: [project]) }
subject { described_class.new(project, user, scope).execute }
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index e2a0c4322ff..c9d2ec8a4ae 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -21,7 +21,9 @@ describe PreferencesHelper do
["Your Projects' Activity", 'project_activity'],
["Starred Projects' Activity", 'starred_project_activity'],
["Your Groups", 'groups'],
- ["Your Todos", 'todos']
+ ["Your Todos", 'todos'],
+ ["Assigned Issues", 'issues'],
+ ["Assigned Merge Requests", 'merge_requests']
]
end
end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 83283f03940..1dc307ea922 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -15,7 +15,7 @@ describe '6_validations' do
describe 'validate_storages_config' do
context 'with correct settings' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/d'))
end
it 'passes through' do
@@ -25,7 +25,7 @@ describe '6_validations' do
context 'when one of the settings is incorrect' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c', 'failure_count_threshold' => 'not a number' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c', 'failure_count_threshold' => 'not a number'))
end
it 'throws an error' do
@@ -35,7 +35,7 @@ describe '6_validations' do
context 'with invalid storage names' do
before do
- mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' })
+ mock_storages('name with spaces' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'))
end
it 'throws an error' do
@@ -67,7 +67,7 @@ describe '6_validations' do
describe 'validate_storages_paths' do
context 'with correct settings' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/d'))
end
it 'passes through' do
@@ -77,7 +77,7 @@ describe '6_validations' do
context 'with nested storage paths' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c/d' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c/d'))
end
it 'throws an error' do
@@ -87,7 +87,7 @@ describe '6_validations' do
context 'with similar but un-nested storage paths' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c2' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c2'))
end
it 'passes through' do
@@ -97,7 +97,7 @@ describe '6_validations' do
describe 'inaccessible storage' do
before do
- mock_storages('foo' => { 'path' => 'tmp/tests/a/path/that/does/not/exist' })
+ mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/a/path/that/does/not/exist'))
end
it 'passes through with a warning' do
diff --git a/spec/initializers/fog_google_https_private_urls_spec.rb b/spec/initializers/fog_google_https_private_urls_spec.rb
new file mode 100644
index 00000000000..de3c157ab7b
--- /dev/null
+++ b/spec/initializers/fog_google_https_private_urls_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe 'Fog::Storage::GoogleXML::File' do
+ let(:storage) do
+ Fog.mock!
+ Fog::Storage.new({
+ google_storage_access_key_id: "asdf",
+ google_storage_secret_access_key: "asdf",
+ provider: "Google"
+ })
+ end
+
+ let(:file) do
+ directory = storage.directories.create(key: 'data')
+ directory.files.create(
+ body: 'Hello World!',
+ key: 'hello_world.txt'
+ )
+ end
+
+ it 'delegates to #get_https_url' do
+ expect(file.url(Time.now)).to start_with("https://")
+ end
+end
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index 838ca9fabef..57f5adbbc40 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -1,5 +1,5 @@
require 'spec_helper'
-require_relative '../../config/initializers/1_settings'
+require_relative '../../config/initializers/1_settings' unless defined?(Settings)
describe Settings do
describe '#ldap' do
diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js
index b8155144e2a..efbe09a10a2 100644
--- a/spec/javascripts/behaviors/copy_as_gfm_spec.js
+++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js
@@ -1,4 +1,4 @@
-import { CopyAsGFM } from '~/behaviors/copy_as_gfm';
+import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
describe('CopyAsGFM', () => {
describe('CopyAsGFM.pasteGFM', () => {
diff --git a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
index 1ea8d86cb7e..94a0c999d66 100644
--- a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
@@ -20,7 +20,7 @@ describe('NativeFormVariableList', () => {
it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
const $row = $wrapper.find('.js-row');
expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('schedule[variables_attributes][][key]');
- expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('schedule[variables_attributes][][value]');
+ expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('schedule[variables_attributes][][secret_value]');
$wrapper.closest('form').trigger('trigger-submit');
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
index a20390c08ee..43d57c2c4dc 100644
--- a/spec/javascripts/fixtures/gl_dropdown.html.haml
+++ b/spec/javascripts/fixtures/gl_dropdown.html.haml
@@ -1,7 +1,8 @@
%div
.dropdown.inline
%button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
- Projects
+ .dropdown-toggle-text
+ Projects
%i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 0e4a7017406..5393502196e 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -256,4 +256,29 @@ describe('glDropdown', function describeDropdown() {
});
});
});
+
+ it('should keep selected item after selecting a second time', () => {
+ const options = {
+ isSelectable(item, $el) {
+ return !$el.hasClass('is-active');
+ },
+ toggleLabel(item) {
+ return item && item.id;
+ },
+ };
+ initDropDown.call(this, false, false, options);
+ const $item = $(`${ITEM_SELECTOR}:first() a`, this.$dropdownMenuElement);
+
+ // select item the first time
+ this.dropdownButtonElement.click();
+ $item.click();
+ expect($item).toHaveClass('is-active');
+ // select item the second time
+ this.dropdownButtonElement.click();
+ $item.click();
+ expect($item).toHaveClass('is-active');
+
+ expect($('.dropdown-toggle-text')).toHaveText(this.projectsData[0].id.toString());
+ });
});
+
diff --git a/spec/javascripts/helpers/vue_component_helper.js b/spec/javascripts/helpers/vue_component_helper.js
new file mode 100644
index 00000000000..257c9f5526a
--- /dev/null
+++ b/spec/javascripts/helpers/vue_component_helper.js
@@ -0,0 +1,3 @@
+export default function removeBreakLine (data) {
+ return data.replace(/\r?\n|\r/g, ' ');
+}
diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js
new file mode 100644
index 00000000000..987aea7befc
--- /dev/null
+++ b/spec/javascripts/ide/components/changed_file_icon_spec.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import changedFileIcon from '~/ide/components/changed_file_icon.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('IDE changed file icon', () => {
+ let vm;
+
+ beforeEach(() => {
+ const component = Vue.extend(changedFileIcon);
+
+ vm = createComponent(component, {
+ file: {
+ tempFile: false,
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('changedIcon', () => {
+ it('equals file-modified when not a temp file', () => {
+ expect(vm.changedIcon).toBe('file-modified');
+ });
+
+ it('equals file-addition when a temp file', () => {
+ vm.file.tempFile = true;
+
+ expect(vm.changedIcon).toBe('file-addition');
+ });
+ });
+
+ describe('changedIconClass', () => {
+ it('includes multi-file-modified when not a temp file', () => {
+ expect(vm.changedIconClass).toContain('multi-file-modified');
+ });
+
+ it('includes multi-file-addition when a temp file', () => {
+ vm.file.tempFile = true;
+
+ expect(vm.changedIconClass).toContain('multi-file-addition');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
new file mode 100644
index 00000000000..144e78d14b5
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import commitActions from '~/ide/components/commit_sidebar/actions.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from 'spec/ide/helpers';
+
+describe('IDE commit sidebar actions', () => {
+ let vm;
+
+ beforeEach(done => {
+ const Component = Vue.extend(commitActions);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.currentBranchId = 'master';
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders 3 groups', () => {
+ expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(3);
+ });
+
+ it('renders current branch text', () => {
+ expect(vm.$el.textContent).toContain('Commit to master branch');
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
new file mode 100644
index 00000000000..5b402886b55
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list collapsed', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(listCollapsed);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.changedFiles.push(file('file1'), file('file2'));
+ vm.$store.state.changedFiles[0].tempFile = true;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders added & modified files count', () => {
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1');
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
new file mode 100644
index 00000000000..15b66952d99
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import listItem from '~/ide/components/commit_sidebar/list_item.vue';
+import router from '~/ide/ide_router';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list item', () => {
+ let vm;
+ let f;
+
+ beforeEach(() => {
+ const Component = Vue.extend(listItem);
+
+ f = file('test-file');
+
+ vm = mountComponent(Component, {
+ file: f,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file path', () => {
+ expect(
+ vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim(),
+ ).toBe(f.path);
+ });
+
+ it('calls discardFileChanges when clicking discard button', () => {
+ spyOn(vm, 'discardFileChanges');
+
+ vm.$el.querySelector('.multi-file-discard-btn').click();
+
+ expect(vm.discardFileChanges).toHaveBeenCalled();
+ });
+
+ it('opens a closed file in the editor when clicking the file path', () => {
+ spyOn(vm, 'openFileInEditor').and.callThrough();
+ spyOn(vm, 'updateViewer');
+ spyOn(router, 'push');
+
+ vm.$el.querySelector('.multi-file-commit-list-path').click();
+
+ expect(vm.openFileInEditor).toHaveBeenCalled();
+ expect(router.push).toHaveBeenCalled();
+ });
+
+ it('calls updateViewer with diff when clicking file', () => {
+ spyOn(vm, 'openFileInEditor').and.callThrough();
+ spyOn(vm, 'updateViewer');
+ spyOn(router, 'push');
+
+ vm.$el.querySelector('.multi-file-commit-list-path').click();
+
+ expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+ });
+
+ describe('computed', () => {
+ describe('iconName', () => {
+ it('returns modified when not a tempFile', () => {
+ expect(vm.iconName).toBe('file-modified');
+ });
+
+ it('returns addition when not a tempFile', () => {
+ f.tempFile = true;
+
+ expect(vm.iconName).toBe('file-addition');
+ });
+ });
+
+ describe('iconClass', () => {
+ it('returns modified when not a tempFile', () => {
+ expect(vm.iconClass).toContain('multi-file-modified');
+ });
+
+ it('returns addition when not a tempFile', () => {
+ f.tempFile = true;
+
+ expect(vm.iconClass).toContain('multi-file-addition');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
new file mode 100644
index 00000000000..a62c0a28340
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file } from '../../helpers';
+
+describe('Multi-file editor commit sidebar list', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(commitSidebarList);
+
+ vm = createComponentWithStore(Component, store, {
+ title: 'Staged',
+ fileList: [],
+ });
+
+ vm.$store.state.rightPanelCollapsed = false;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with a list of files', () => {
+ beforeEach(done => {
+ const f = file('file name');
+ f.changed = true;
+ vm.fileList.push(f);
+
+ Vue.nextTick(done);
+ });
+
+ it('renders list', () => {
+ expect(vm.$el.querySelectorAll('li').length).toBe(1);
+ });
+ });
+
+ describe('collapsed', () => {
+ beforeEach(done => {
+ vm.$store.state.rightPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('hides list', () => {
+ expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
+ expect(vm.$el.querySelector('.help-block')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
new file mode 100644
index 00000000000..4e8243439f3
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
@@ -0,0 +1,130 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from 'spec/ide/helpers';
+
+describe('IDE commit sidebar radio group', () => {
+ let vm;
+
+ beforeEach(done => {
+ const Component = Vue.extend(radioGroup);
+
+ store.state.commit.commitAction = '2';
+
+ vm = createComponentWithStore(Component, store, {
+ value: '1',
+ label: 'test',
+ checked: true,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('uses label if present', () => {
+ expect(vm.$el.textContent).toContain('test');
+ });
+
+ it('uses slot if label is not present', done => {
+ vm.$destroy();
+
+ vm = new Vue({
+ components: {
+ radioGroup,
+ },
+ store,
+ template: `
+ <radio-group
+ value="1"
+ >
+ Testing slot
+ </radio-group>
+ `,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.textContent).toContain('Testing slot');
+
+ done();
+ });
+ });
+
+ it('updates store when changing radio button', done => {
+ vm.$el.querySelector('input').dispatchEvent(new Event('change'));
+
+ Vue.nextTick(() => {
+ expect(store.state.commit.commitAction).toBe('1');
+
+ done();
+ });
+ });
+
+ it('renders helpText tooltip', done => {
+ vm.helpText = 'help text';
+
+ Vue.nextTick(() => {
+ const help = vm.$el.querySelector('.help-block');
+
+ expect(help).not.toBeNull();
+ expect(help.getAttribute('data-original-title')).toBe('help text');
+
+ done();
+ });
+ });
+
+ describe('with input', () => {
+ beforeEach(done => {
+ vm.$destroy();
+
+ const Component = Vue.extend(radioGroup);
+
+ store.state.commit.commitAction = '1';
+
+ vm = createComponentWithStore(Component, store, {
+ value: '1',
+ label: 'test',
+ checked: true,
+ showInput: true,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders input box when commitAction matches value', () => {
+ expect(vm.$el.querySelector('.form-control')).not.toBeNull();
+ });
+
+ it('hides input when commitAction doesnt match value', done => {
+ store.state.commit.commitAction = '2';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.form-control')).toBeNull();
+ done();
+ });
+ });
+
+ it('updates branch name in store on input', done => {
+ const input = vm.$el.querySelector('.form-control');
+ input.value = 'testing-123';
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick(() => {
+ expect(store.state.commit.newBranchName).toBe('testing-123');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_context_bar_spec.js b/spec/javascripts/ide/components/ide_context_bar_spec.js
new file mode 100644
index 00000000000..e17b051f137
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_context_bar_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideContextBar from '~/ide/components/ide_context_bar.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+
+describe('Multi-file editor right context bar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideContextBar);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'svg',
+ });
+
+ vm.$store.state.rightPanelCollapsed = false;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed', () => {
+ beforeEach(done => {
+ vm.$store.state.rightPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('adds collapsed class', () => {
+ expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_external_links_spec.js b/spec/javascripts/ide/components/ide_external_links_spec.js
new file mode 100644
index 00000000000..9f6cb459f3b
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_external_links_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import ideExternalLinks from '~/ide/components/ide_external_links.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('ide external links component', () => {
+ let vm;
+ let fakeReferrer;
+ let Component;
+
+ const fakeProjectUrl = '/project/';
+
+ beforeEach(() => {
+ Component = Vue.extend(ideExternalLinks);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('goBackUrl', () => {
+ it('renders the Go Back link with the referrer when present', () => {
+ fakeReferrer = '/example/README.md';
+ spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
+
+ vm = createComponent(Component, {
+ projectUrl: fakeProjectUrl,
+ }).$mount();
+
+ expect(vm.goBackUrl).toEqual(fakeReferrer);
+ });
+
+ it('renders the Go Back link with the project url when referrer is not present', () => {
+ fakeReferrer = '';
+ spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer);
+
+ vm = createComponent(Component, {
+ projectUrl: fakeProjectUrl,
+ }).$mount();
+
+ expect(vm.goBackUrl).toEqual(fakeProjectUrl);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_project_tree_spec.js b/spec/javascripts/ide/components/ide_project_tree_spec.js
new file mode 100644
index 00000000000..657682cb39c
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_project_tree_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import ProjectTree from '~/ide/components/ide_project_tree.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('IDE project tree', () => {
+ const Component = Vue.extend(ProjectTree);
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent(Component, {
+ project: {
+ id: 1,
+ name: 'test',
+ web_url: gl.TEST_HOST,
+ avatar_url: '',
+ branches: [],
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders identicon when projct has no avatar', () => {
+ expect(vm.$el.querySelector('.identicon')).not.toBeNull();
+ });
+
+ it('renders avatar image if project has avatar', done => {
+ vm.project.avatar_url = gl.TEST_HOST;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.identicon')).toBeNull();
+ expect(vm.$el.querySelector('img.avatar')).not.toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_repo_tree_spec.js b/spec/javascripts/ide/components/ide_repo_tree_spec.js
new file mode 100644
index 00000000000..e0fbc90ca61
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_repo_tree_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
+import createComponent from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('IdeRepoTree', () => {
+ let vm;
+ let tree;
+
+ beforeEach(() => {
+ const IdeRepoTree = Vue.extend(ideRepoTree);
+
+ tree = {
+ tree: [file()],
+ loading: false,
+ };
+
+ vm = createComponent(IdeRepoTree, {
+ tree,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a sidebar', () => {
+ expect(vm.$el.querySelector('.loading-file')).toBeNull();
+ expect(vm.$el.querySelector('.file')).not.toBeNull();
+ });
+
+ it('renders 3 loading files if tree is loading', done => {
+ tree.loading = true;
+
+ vm.$nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('.multi-file-loading-container').length,
+ ).toEqual(3);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_side_bar_spec.js b/spec/javascripts/ide/components/ide_side_bar_spec.js
new file mode 100644
index 00000000000..699dae1ce2f
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_side_bar_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideSidebar from '~/ide/components/ide_side_bar.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+
+describe('IdeSidebar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideSidebar);
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders a sidebar', () => {
+ expect(
+ vm.$el.querySelector('.multi-file-commit-panel-inner'),
+ ).not.toBeNull();
+ });
+
+ it('renders loading icon component', done => {
+ vm.$store.state.loading = true;
+
+ vm.$nextTick(() => {
+ expect(
+ vm.$el.querySelector('.multi-file-loading-container'),
+ ).not.toBeNull();
+ expect(
+ vm.$el.querySelectorAll('.multi-file-loading-container').length,
+ ).toBe(3);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
new file mode 100644
index 00000000000..5bd890094cc
--- /dev/null
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ide from '~/ide/components/ide.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file, resetStore } from '../helpers';
+
+describe('ide component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ide);
+
+ vm = createComponentWithStore(Component, store, {
+ emptyStateSvgPath: 'svg',
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'svg',
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('does not render panel right when no files open', () => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
+ });
+
+ it('renders panel right when files are open', done => {
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [file()],
+ };
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.panel-right')).toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js
new file mode 100644
index 00000000000..e08abe7d849
--- /dev/null
+++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import newDropdown from '~/ide/components/new_dropdown/index.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('new dropdown component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const component = Vue.extend(newDropdown);
+
+ vm = createComponentWithStore(component, store, {
+ branch: 'master',
+ path: '',
+ });
+
+ vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.path = '';
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders new file, upload and new directory links', () => {
+ expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
+ expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe(
+ 'Upload file',
+ );
+ expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe(
+ 'New directory',
+ );
+ });
+
+ describe('createNewItem', () => {
+ it('sets modalType to blob when new file is clicked', () => {
+ vm.$el.querySelectorAll('a')[0].click();
+
+ expect(vm.modalType).toBe('blob');
+ });
+
+ it('sets modalType to tree when new directory is clicked', () => {
+ vm.$el.querySelectorAll('a')[2].click();
+
+ expect(vm.modalType).toBe('tree');
+ });
+
+ it('opens modal when link is clicked', done => {
+ vm.$el.querySelectorAll('a')[0].click();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.modal')).not.toBeNull();
+
+ done();
+ });
+ });
+ });
+
+ describe('hideModal', () => {
+ beforeAll(done => {
+ vm.openModal = true;
+ Vue.nextTick(done);
+ });
+
+ it('closes modal after toggling', done => {
+ vm.hideModal();
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.modal')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
new file mode 100644
index 00000000000..a6e1e5a0d35
--- /dev/null
+++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+import modal from '~/ide/components/new_dropdown/modal.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('new file modal component', () => {
+ const Component = Vue.extend(modal);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ ['tree', 'blob'].forEach(type => {
+ describe(type, () => {
+ beforeEach(() => {
+ vm = createComponent(Component, {
+ type,
+ branchId: 'master',
+ path: '',
+ });
+
+ vm.entryName = 'testing';
+ });
+
+ it(`sets modal title as ${type}`, () => {
+ const title = type === 'tree' ? 'directory' : 'file';
+
+ expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(
+ `Create new ${title}`,
+ );
+ });
+
+ it(`sets button label as ${type}`, () => {
+ const title = type === 'tree' ? 'directory' : 'file';
+
+ expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(
+ `Create ${title}`,
+ );
+ });
+
+ it(`sets form label as ${type}`, () => {
+ const title = type === 'tree' ? 'Directory' : 'File';
+
+ expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(
+ `${title} name`,
+ );
+ });
+
+ describe('createEntryInStore', () => {
+ it('$emits create', () => {
+ spyOn(vm, '$emit');
+
+ vm.createEntryInStore();
+
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ branchId: 'master',
+ name: 'testing',
+ type,
+ });
+ });
+ });
+ });
+ });
+
+ it('focuses field on mount', () => {
+ document.body.innerHTML += '<div class="js-test"></div>';
+
+ vm = createComponent(
+ Component,
+ {
+ type: 'tree',
+ branchId: 'master',
+ path: '',
+ },
+ '.js-test',
+ );
+
+ expect(document.activeElement).toBe(vm.$refs.fieldName);
+
+ vm.$el.remove();
+ });
+});
diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
new file mode 100644
index 00000000000..2bc5d701601
--- /dev/null
+++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
@@ -0,0 +1,87 @@
+import Vue from 'vue';
+import upload from '~/ide/components/new_dropdown/upload.vue';
+import createComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('new dropdown upload', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(upload);
+
+ vm = createComponent(Component, {
+ branchId: 'master',
+ path: '',
+ });
+
+ vm.entryName = 'testing';
+
+ spyOn(vm, '$emit');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('readFile', () => {
+ beforeEach(() => {
+ spyOn(FileReader.prototype, 'readAsText');
+ spyOn(FileReader.prototype, 'readAsDataURL');
+ });
+
+ it('calls readAsText for text files', () => {
+ const file = {
+ type: 'text/html',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file);
+ });
+
+ it('calls readAsDataURL for non-text files', () => {
+ const file = {
+ type: 'images/png',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
+ });
+ });
+
+ describe('createFile', () => {
+ const target = {
+ result: 'content',
+ };
+ const binaryTarget = {
+ result: 'base64,base64content',
+ };
+ const file = {
+ name: 'file',
+ };
+
+ it('creates new file', () => {
+ vm.createFile(target, file, true);
+
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ name: file.name,
+ branchId: 'master',
+ type: 'blob',
+ content: target.result,
+ base64: false,
+ });
+ });
+
+ it('splits content on base64 if binary', () => {
+ vm.createFile(binaryTarget, file, false);
+
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ name: file.name,
+ branchId: 'master',
+ type: 'blob',
+ content: binaryTarget.result.split('base64,')[1],
+ base64: true,
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
new file mode 100644
index 00000000000..113ade269e9
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -0,0 +1,173 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import repoCommitSection from '~/ide/components/repo_commit_section.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
+import { file, resetStore } from '../helpers';
+
+describe('RepoCommitSection', () => {
+ let vm;
+
+ function createComponent() {
+ const Component = Vue.extend(repoCommitSection);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'commitsvg',
+ });
+
+ vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.currentBranchId = 'master';
+ vm.$store.state.projects.abcproject = {
+ web_url: '',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+
+ vm.$store.state.rightPanelCollapsed = false;
+ vm.$store.state.currentBranch = 'master';
+ vm.$store.state.changedFiles = [file('file1'), file('file2')];
+ vm.$store.state.changedFiles.forEach(f =>
+ Object.assign(f, {
+ changed: true,
+ content: 'testing',
+ }),
+ );
+
+ return vm.$mount();
+ }
+
+ beforeEach(done => {
+ vm = createComponent();
+
+ spyOn(service, 'getTreeData').and.returnValue(
+ Promise.resolve({
+ headers: {
+ 'page-title': 'test',
+ },
+ json: () =>
+ Promise.resolve({
+ last_commit_path: 'last_commit_path',
+ parent_tree_url: 'parent_tree_url',
+ path: '/',
+ trees: [{ name: 'tree' }],
+ blobs: [{ name: 'blob' }],
+ submodules: [{ name: 'submodule' }],
+ }),
+ }),
+ );
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('empty Stage', () => {
+ it('renders no changes text', () => {
+ resetStore(vm.$store);
+ const Component = Vue.extend(repoCommitSection);
+
+ vm = createComponentWithStore(Component, store, {
+ noChangesStateSvgPath: 'nochangessvg',
+ committedStateSvgPath: 'svg',
+ }).$mount();
+
+ expect(
+ vm.$el.querySelector('.js-empty-state').textContent.trim(),
+ ).toContain('No changes');
+ expect(
+ vm.$el.querySelector('.js-empty-state img').getAttribute('src'),
+ ).toBe('nochangessvg');
+ });
+ });
+
+ it('renders a commit section', () => {
+ const changedFileElements = [
+ ...vm.$el.querySelectorAll('.multi-file-commit-list li'),
+ ];
+ const submitCommit = vm.$el.querySelector('form .btn');
+
+ expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
+ expect(changedFileElements.length).toEqual(2);
+
+ changedFileElements.forEach((changedFile, i) => {
+ expect(changedFile.textContent.trim()).toContain(
+ vm.$store.state.changedFiles[i].path,
+ );
+ });
+
+ expect(submitCommit.disabled).toBeTruthy();
+ expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
+ });
+
+ it('updates commitMessage in store on input', done => {
+ const textarea = vm.$el.querySelector('textarea');
+
+ textarea.value = 'testing commit message';
+
+ textarea.dispatchEvent(new Event('input'));
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(vm.$store.state.commit.commitMessage).toBe(
+ 'testing commit message',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('discard draft button', () => {
+ it('hidden when commitMessage is empty', () => {
+ expect(
+ vm.$el.querySelector('.multi-file-commit-form .btn-default'),
+ ).toBeNull();
+ });
+
+ it('resets commitMessage when clicking discard button', done => {
+ vm.$store.state.commit.commitMessage = 'testing commit message';
+
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.multi-file-commit-form .btn-default').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.$store.state.commit.commitMessage).not.toBe(
+ 'testing commit message',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('when submitting', () => {
+ beforeEach(() => {
+ spyOn(vm, 'commitChanges');
+ });
+
+ it('calls commitChanges', done => {
+ vm.$store.state.commit.commitMessage = 'testing commit message';
+
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.multi-file-commit-form .btn-success').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.commitChanges).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
new file mode 100644
index 00000000000..ae657e8c881
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -0,0 +1,137 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoEditor from '~/ide/components/repo_editor.vue';
+import monacoLoader from '~/ide/monaco_loader';
+import Editor from '~/ide/lib/editor';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { file, resetStore } from '../helpers';
+
+describe('RepoEditor', () => {
+ let vm;
+
+ beforeEach(done => {
+ const f = file();
+ const RepoEditor = Vue.extend(repoEditor);
+
+ vm = createComponentWithStore(RepoEditor, store, {
+ file: f,
+ });
+
+ f.active = true;
+ f.tempFile = true;
+ f.html = 'testing';
+ vm.$store.state.openFiles.push(f);
+ vm.$store.state.entries[f.path] = f;
+ vm.monaco = true;
+
+ vm.$mount();
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ setTimeout(done, 0);
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+
+ Editor.editorInstance.modelManager.dispose();
+ });
+
+ it('renders an ide container', done => {
+ Vue.nextTick(() => {
+ expect(vm.shouldHideEditor).toBeFalsy();
+
+ done();
+ });
+ });
+
+ describe('when open file is binary and not raw', () => {
+ beforeEach(done => {
+ vm.file.binary = true;
+
+ vm.$nextTick(done);
+ });
+
+ it('does not render the IDE', () => {
+ expect(vm.shouldHideEditor).toBeTruthy();
+ });
+
+ it('shows activeFile html', () => {
+ expect(vm.$el.textContent).toContain('testing');
+ });
+ });
+
+ describe('createEditorInstance', () => {
+ it('calls createInstance when viewer is editor', done => {
+ spyOn(vm.editor, 'createInstance');
+
+ vm.createEditorInstance();
+
+ vm.$nextTick(() => {
+ expect(vm.editor.createInstance).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('calls createDiffInstance when viewer is diff', done => {
+ vm.$store.state.viewer = 'diff';
+
+ spyOn(vm.editor, 'createDiffInstance');
+
+ vm.createEditorInstance();
+
+ vm.$nextTick(() => {
+ expect(vm.editor.createDiffInstance).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('setupEditor', () => {
+ it('creates new model', () => {
+ spyOn(vm.editor, 'createModel').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file);
+ expect(vm.model).not.toBeNull();
+ });
+
+ it('attaches model to editor', () => {
+ spyOn(vm.editor, 'attachModel').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model);
+ });
+
+ it('adds callback methods', () => {
+ spyOn(vm.editor, 'onPositionChange').and.callThrough();
+
+ Editor.editorInstance.modelManager.dispose();
+
+ vm.setupEditor();
+
+ expect(vm.editor.onPositionChange).toHaveBeenCalled();
+ expect(vm.model.events.size).toBe(1);
+ });
+
+ it('updates state when model content changed', done => {
+ vm.model.setValue('testing 123');
+
+ setTimeout(() => {
+ expect(vm.file.content).toBe('testing 123');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_file_buttons_spec.js b/spec/javascripts/ide/components/repo_file_buttons_spec.js
new file mode 100644
index 00000000000..c86bdb132b4
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_file_buttons_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import repoFileButtons from '~/ide/components/repo_file_buttons.vue';
+import createVueComponent from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('RepoFileButtons', () => {
+ const activeFile = file();
+ let vm;
+
+ function createComponent() {
+ const RepoFileButtons = Vue.extend(repoFileButtons);
+
+ activeFile.rawPath = 'test';
+ activeFile.blamePath = 'test';
+ activeFile.commitsPath = 'test';
+
+ return createVueComponent(RepoFileButtons, {
+ file: activeFile,
+ });
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders Raw, Blame, History, Permalink and Preview toggle', done => {
+ vm = createComponent();
+
+ vm.$nextTick(() => {
+ const raw = vm.$el.querySelector('.raw');
+ const blame = vm.$el.querySelector('.blame');
+ const history = vm.$el.querySelector('.history');
+
+ expect(raw.href).toMatch(`/${activeFile.rawPath}`);
+ expect(raw.textContent.trim()).toEqual('Raw');
+ expect(blame.href).toMatch(`/${activeFile.blamePath}`);
+ expect(blame.textContent.trim()).toEqual('Blame');
+ expect(history.href).toMatch(`/${activeFile.commitsPath}`);
+ expect(history.textContent.trim()).toEqual('History');
+ expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual(
+ 'Permalink',
+ );
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js
new file mode 100644
index 00000000000..ff391cb4351
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_file_spec.js
@@ -0,0 +1,80 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoFile from '~/ide/components/repo_file.vue';
+import router from '~/ide/ide_router';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('RepoFile', () => {
+ let vm;
+
+ function createComponent(propsData) {
+ const RepoFile = Vue.extend(repoFile);
+
+ vm = createComponentWithStore(RepoFile, store, propsData);
+
+ vm.$mount();
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders link, icon and name', () => {
+ createComponent({
+ file: file('t4'),
+ level: 0,
+ });
+
+ const name = vm.$el.querySelector('.ide-file-name');
+
+ expect(name.href).toMatch('');
+ expect(name.textContent.trim()).toEqual(vm.file.name);
+ });
+
+ it('fires clickFile when the link is clicked', done => {
+ spyOn(router, 'push');
+ createComponent({
+ file: file('t3'),
+ level: 0,
+ });
+
+ vm.$el.querySelector('.file-name').click();
+
+ setTimeout(() => {
+ expect(router.push).toHaveBeenCalledWith(`/project${vm.file.url}`);
+
+ done();
+ });
+ });
+
+ describe('locked file', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file('locked file');
+ f.file_lock = {
+ user: {
+ name: 'testuser',
+ updated_at: new Date(),
+ },
+ };
+
+ createComponent({
+ file: f,
+ level: 0,
+ });
+ });
+
+ it('renders lock icon', () => {
+ expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
+ });
+
+ it('renders a tooltip', () => {
+ expect(
+ vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset
+ .originalTitle,
+ ).toContain('Locked by testuser');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_loading_file_spec.js b/spec/javascripts/ide/components/repo_loading_file_spec.js
new file mode 100644
index 00000000000..8f9644216bc
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_loading_file_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoLoadingFile from '~/ide/components/repo_loading_file.vue';
+import { resetStore } from '../helpers';
+
+describe('RepoLoadingFile', () => {
+ let vm;
+
+ function createComponent() {
+ const RepoLoadingFile = Vue.extend(repoLoadingFile);
+
+ return new RepoLoadingFile({
+ store,
+ }).$mount();
+ }
+
+ function assertLines(lines) {
+ lines.forEach((line, n) => {
+ const index = n + 1;
+ expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy();
+ });
+ }
+
+ function assertColumns(columns) {
+ columns.forEach(column => {
+ const container = column.querySelector('.animation-container');
+ const lines = [...container.querySelectorAll(':scope > div')];
+
+ expect(container).toBeTruthy();
+ expect(lines.length).toEqual(6);
+ assertLines(lines);
+ });
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders 3 columns of animated LoC', () => {
+ vm = createComponent();
+ const columns = [...vm.$el.querySelectorAll('td')];
+
+ expect(columns.length).toEqual(3);
+ assertColumns(columns);
+ });
+
+ it('renders 1 column of animated LoC if isMini', done => {
+ vm = createComponent();
+ vm.$store.state.leftPanelCollapsed = true;
+ vm.$store.state.openFiles.push('test');
+
+ vm.$nextTick(() => {
+ const columns = [...vm.$el.querySelectorAll('td')];
+
+ expect(columns.length).toEqual(1);
+ assertColumns(columns);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
new file mode 100644
index 00000000000..ddb5204e3a7
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_tab_spec.js
@@ -0,0 +1,165 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoTab from '~/ide/components/repo_tab.vue';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../helpers';
+
+describe('RepoTab', () => {
+ let vm;
+
+ function createComponent(propsData) {
+ const RepoTab = Vue.extend(repoTab);
+
+ return new RepoTab({
+ store,
+ propsData,
+ }).$mount();
+ }
+
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders a close link and a name link', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+ vm.$store.state.openFiles.push(vm.tab);
+ const close = vm.$el.querySelector('.multi-file-tab-close');
+ const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`);
+
+ expect(close.innerHTML).toContain('#close');
+ expect(name.textContent.trim()).toEqual(vm.tab.name);
+ });
+
+ it('fires clickFile when the link is clicked', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'clickFile');
+
+ vm.$el.click();
+
+ expect(vm.clickFile).toHaveBeenCalledWith(vm.tab);
+ });
+
+ it('calls closeFile when clicking close button', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ spyOn(vm, 'closeFile');
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ expect(vm.closeFile).toHaveBeenCalledWith(vm.tab.path);
+ });
+
+ it('changes icon on hover', done => {
+ const tab = file();
+ tab.changed = true;
+ vm = createComponent({
+ tab,
+ });
+
+ vm.$el.dispatchEvent(new Event('mouseover'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.multi-file-modified')).toBeNull();
+
+ vm.$el.dispatchEvent(new Event('mouseout'));
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.$el.querySelector('.multi-file-modified')).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ describe('locked file', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file('locked file');
+ f.file_lock = {
+ user: {
+ name: 'testuser',
+ updated_at: new Date(),
+ },
+ };
+
+ vm = createComponent({
+ tab: f,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders lock icon', () => {
+ expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
+ });
+
+ it('renders a tooltip', () => {
+ expect(
+ vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle,
+ ).toContain('Locked by testuser');
+ });
+ });
+
+ describe('methods', () => {
+ describe('closeTab', () => {
+ it('closes tab if file has changed', done => {
+ const tab = file();
+ tab.changed = true;
+ tab.opened = true;
+ vm = createComponent({
+ tab,
+ });
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.state.changedFiles.push(tab);
+ vm.$store.state.entries[tab.path] = tab;
+ vm.$store.dispatch('setFileActive', tab.path);
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
+ expect(vm.$store.state.changedFiles.length).toBe(1);
+
+ done();
+ });
+ });
+
+ it('closes tab when clicking close btn', done => {
+ const tab = file('lose');
+ tab.opened = true;
+ vm = createComponent({
+ tab,
+ });
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.state.entries[tab.path] = tab;
+ vm.$store.dispatch('setFileActive', tab.path);
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
new file mode 100644
index 00000000000..ceb0416aff8
--- /dev/null
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+import repoTabs from '~/ide/components/repo_tabs.vue';
+import createComponent from '../../helpers/vue_mount_component_helper';
+import { file } from '../helpers';
+
+describe('RepoTabs', () => {
+ const openedFiles = [file('open1'), file('open2')];
+ const RepoTabs = Vue.extend(repoTabs);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a list of tabs', done => {
+ vm = createComponent(RepoTabs, {
+ files: openedFiles,
+ viewer: 'editor',
+ hasChanges: false,
+ });
+ openedFiles[0].active = true;
+
+ vm.$nextTick(() => {
+ const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
+
+ expect(tabs.length).toEqual(2);
+ expect(tabs[0].classList.contains('active')).toEqual(true);
+ expect(tabs[1].classList.contains('active')).toEqual(false);
+
+ done();
+ });
+ });
+
+ describe('updated', () => {
+ it('sets showShadow as true when scroll width is larger than width', done => {
+ const el = document.createElement('div');
+ el.innerHTML = '<div id="test-app"></div>';
+ document.body.appendChild(el);
+
+ const style = document.createElement('style');
+ style.innerText = `
+ .multi-file-tabs {
+ width: 100px;
+ }
+
+ .multi-file-tabs .list-unstyled {
+ display: flex;
+ overflow-x: auto;
+ }
+ `;
+ document.head.appendChild(style);
+
+ vm = createComponent(
+ RepoTabs,
+ {
+ files: [],
+ viewer: 'editor',
+ hasChanges: false,
+ },
+ '#test-app',
+ );
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.showShadow).toEqual(false);
+
+ vm.files = openedFiles;
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.showShadow).toEqual(true);
+
+ style.remove();
+ el.remove();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js
new file mode 100644
index 00000000000..98db6defc7a
--- /dev/null
+++ b/spec/javascripts/ide/helpers.js
@@ -0,0 +1,22 @@
+import { decorateData } from '~/ide/stores/utils';
+import state from '~/ide/stores/state';
+import commitState from '~/ide/stores/modules/commit/state';
+
+export const resetStore = store => {
+ const newState = {
+ ...state(),
+ commit: commitState(),
+ };
+ store.replaceState(newState);
+};
+
+export const file = (name = 'name', id = name, type = '') =>
+ decorateData({
+ id,
+ type,
+ icon: 'icon',
+ url: 'url',
+ name,
+ path: name,
+ lastCommit: {},
+ });
diff --git a/spec/javascripts/ide/lib/common/disposable_spec.js b/spec/javascripts/ide/lib/common/disposable_spec.js
new file mode 100644
index 00000000000..af12ca15369
--- /dev/null
+++ b/spec/javascripts/ide/lib/common/disposable_spec.js
@@ -0,0 +1,44 @@
+import Disposable from '~/ide/lib/common/disposable';
+
+describe('Multi-file editor library disposable class', () => {
+ let instance;
+ let disposableClass;
+
+ beforeEach(() => {
+ instance = new Disposable();
+
+ disposableClass = {
+ dispose: jasmine.createSpy('dispose'),
+ };
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('add', () => {
+ it('adds disposable classes', () => {
+ instance.add(disposableClass);
+
+ expect(instance.disposers.size).toBe(1);
+ });
+ });
+
+ describe('dispose', () => {
+ beforeEach(() => {
+ instance.add(disposableClass);
+ });
+
+ it('calls dispose on all cached disposers', () => {
+ instance.dispose();
+
+ expect(disposableClass.dispose).toHaveBeenCalled();
+ });
+
+ it('clears cached disposers', () => {
+ instance.dispose();
+
+ expect(instance.disposers.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js
new file mode 100644
index 00000000000..4381f6fcfd0
--- /dev/null
+++ b/spec/javascripts/ide/lib/common/model_manager_spec.js
@@ -0,0 +1,129 @@
+/* global monaco */
+import eventHub from '~/ide/eventhub';
+import monacoLoader from '~/ide/monaco_loader';
+import ModelManager from '~/ide/lib/common/model_manager';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model manager', () => {
+ let instance;
+
+ beforeEach(done => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = new ModelManager(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('addModel', () => {
+ it('caches model', () => {
+ instance.addModel(file());
+
+ expect(instance.models.size).toBe(1);
+ });
+
+ it('caches model by file path', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.models.keys().next().value).toBe('path-name');
+ });
+
+ it('adds model into disposable', () => {
+ spyOn(instance.disposable, 'add').and.callThrough();
+
+ instance.addModel(file());
+
+ expect(instance.disposable.add).toHaveBeenCalled();
+ });
+
+ it('returns cached model', () => {
+ spyOn(instance.models, 'get').and.callThrough();
+
+ instance.addModel(file());
+ instance.addModel(file());
+
+ expect(instance.models.get).toHaveBeenCalled();
+ });
+
+ it('adds eventHub listener', () => {
+ const f = file();
+ spyOn(eventHub, '$on').and.callThrough();
+
+ instance.addModel(f);
+
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${f.path}`,
+ jasmine.anything(),
+ );
+ });
+ });
+
+ describe('hasCachedModel', () => {
+ it('returns false when no models exist', () => {
+ expect(instance.hasCachedModel('path')).toBeFalsy();
+ });
+
+ it('returns true when model exists', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.hasCachedModel('path-name')).toBeTruthy();
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns cached model', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.getModel('path-name')).not.toBeNull();
+ });
+ });
+
+ describe('removeCachedModel', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file();
+
+ instance.addModel(f);
+ });
+
+ it('clears cached model', () => {
+ instance.removeCachedModel(f);
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('removes eventHub listener', () => {
+ spyOn(eventHub, '$off').and.callThrough();
+
+ instance.removeCachedModel(f);
+
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${f.path}`,
+ jasmine.anything(),
+ );
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached models', () => {
+ instance.addModel(file());
+
+ instance.dispose();
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('calls disposable dispose', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
new file mode 100644
index 00000000000..adc6a93c06b
--- /dev/null
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -0,0 +1,113 @@
+/* global monaco */
+import eventHub from '~/ide/eventhub';
+import monacoLoader from '~/ide/monaco_loader';
+import Model from '~/ide/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model', () => {
+ let model;
+
+ beforeEach(done => {
+ spyOn(eventHub, '$on').and.callThrough();
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ });
+
+ it('creates original model & new model', () => {
+ expect(model.originalModel).not.toBeNull();
+ expect(model.model).not.toBeNull();
+ });
+
+ it('adds eventHub listener', () => {
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${model.file.path}`,
+ jasmine.anything(),
+ );
+ });
+
+ describe('path', () => {
+ it('returns file path', () => {
+ expect(model.path).toBe('path');
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns model', () => {
+ expect(model.getModel()).toBe(model.model);
+ });
+ });
+
+ describe('getOriginalModel', () => {
+ it('returns original model', () => {
+ expect(model.getOriginalModel()).toBe(model.originalModel);
+ });
+ });
+
+ describe('setValue', () => {
+ it('updates models value', () => {
+ model.setValue('testing 123');
+
+ expect(model.getModel().getValue()).toBe('testing 123');
+ });
+ });
+
+ describe('onChange', () => {
+ it('caches event by path', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+ expect(model.events.keys().next().value).toBe('path');
+ });
+
+ it('calls callback on change', done => {
+ const spy = jasmine.createSpy();
+ model.onChange(spy);
+
+ model.getModel().setValue('123');
+
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalledWith(model, jasmine.anything());
+ done();
+ });
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(model.disposable, 'dispose').and.callThrough();
+
+ model.dispose();
+
+ expect(model.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('clears events', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+
+ model.dispose();
+
+ expect(model.events.size).toBe(0);
+ });
+
+ it('removes eventHub listener', () => {
+ spyOn(eventHub, '$off').and.callThrough();
+
+ model.dispose();
+
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${model.file.path}`,
+ jasmine.anything(),
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
new file mode 100644
index 00000000000..092170d086a
--- /dev/null
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -0,0 +1,139 @@
+/* global monaco */
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import Model from '~/ide/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library decorations controller', () => {
+ let editorInstance;
+ let controller;
+ let model;
+
+ beforeEach(done => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ controller = new DecorationsController(editorInstance);
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ editorInstance.dispose();
+ controller.dispose();
+ });
+
+ describe('getAllDecorationsForModel', () => {
+ it('returns empty array when no decorations exist for model', () => {
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations).toEqual([]);
+ });
+
+ it('returns decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations[0]).toEqual({ decoration: 'decorationValue' });
+ });
+ });
+
+ describe('addDecorations', () => {
+ it('caches decorations in a new map', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('does not create new cache model', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue2' },
+ ]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ expect(controller.decorations.size).toBe(1);
+ expect(controller.decorations.keys().next().value).toBe('path');
+ });
+
+ it('calls decorate method', () => {
+ spyOn(controller, 'decorate');
+
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ expect(controller.decorate).toHaveBeenCalled();
+ });
+ });
+
+ describe('decorate', () => {
+ it('sets decorations on editor instance', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations');
+
+ controller.decorate(model);
+
+ expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith(
+ [],
+ [],
+ );
+ });
+
+ it('caches decorations', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.keys().next().value).toBe('path');
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached decorations', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ controller.dispose();
+
+ expect(controller.decorations.size).toBe(0);
+ });
+
+ it('clears cached editorDecorations', () => {
+ controller.addDecorations(model, 'key', [
+ { decoration: 'decorationValue' },
+ ]);
+
+ controller.dispose();
+
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
new file mode 100644
index 00000000000..c8f3e9f4830
--- /dev/null
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -0,0 +1,196 @@
+/* global monaco */
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import ModelManager from '~/ide/lib/common/model_manager';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import DirtyDiffController, {
+ getDiffChangeType,
+ getDecorator,
+} from '~/ide/lib/diff/controller';
+import { computeDiff } from '~/ide/lib/diff/diff';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library dirty diff controller', () => {
+ let editorInstance;
+ let controller;
+ let modelManager;
+ let decorationsController;
+ let model;
+
+ beforeEach(done => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ modelManager = new ModelManager(monaco);
+ decorationsController = new DecorationsController(editorInstance);
+
+ model = modelManager.addModel(file('path'));
+
+ controller = new DirtyDiffController(modelManager, decorationsController);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ controller.dispose();
+ model.dispose();
+ decorationsController.dispose();
+ editorInstance.dispose();
+ });
+
+ describe('getDiffChangeType', () => {
+ ['added', 'removed', 'modified'].forEach(type => {
+ it(`returns ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDiffChangeType(change)).toBe(type);
+ });
+ });
+ });
+
+ describe('getDecorator', () => {
+ ['added', 'removed', 'modified'].forEach(type => {
+ it(`returns with linesDecorationsClassName for ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDecorator(change).options.linesDecorationsClassName).toBe(
+ `dirty-diff dirty-diff-${type}`,
+ );
+ });
+
+ it('returns with line numbers', () => {
+ const change = {
+ lineNumber: 1,
+ endLineNumber: 2,
+ [type]: true,
+ };
+
+ const range = getDecorator(change).range;
+
+ expect(range.startLineNumber).toBe(1);
+ expect(range.endLineNumber).toBe(2);
+ expect(range.startColumn).toBe(1);
+ expect(range.endColumn).toBe(1);
+ });
+ });
+ });
+
+ describe('attachModel', () => {
+ it('adds change event callback', () => {
+ spyOn(model, 'onChange');
+
+ controller.attachModel(model);
+
+ expect(model.onChange).toHaveBeenCalled();
+ });
+
+ it('calls throttledComputeDiff on change', () => {
+ spyOn(controller, 'throttledComputeDiff');
+
+ controller.attachModel(model);
+
+ model.getModel().setValue('123');
+
+ expect(controller.throttledComputeDiff).toHaveBeenCalled();
+ });
+ });
+
+ describe('computeDiff', () => {
+ it('posts to worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'postMessage');
+
+ controller.computeDiff(model);
+
+ expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({
+ path: model.path,
+ originalContent: '',
+ newContent: '',
+ });
+ });
+ });
+
+ describe('reDecorate', () => {
+ it('calls decorations controller decorate', () => {
+ spyOn(controller.decorationsController, 'decorate');
+
+ controller.reDecorate(model);
+
+ expect(controller.decorationsController.decorate).toHaveBeenCalledWith(
+ model,
+ );
+ });
+ });
+
+ describe('decorate', () => {
+ it('adds decorations into decorations controller', () => {
+ spyOn(controller.decorationsController, 'addDecorations');
+
+ controller.decorate({ data: { changes: [], path: 'path' } });
+
+ expect(
+ controller.decorationsController.addDecorations,
+ ).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything());
+ });
+
+ it('adds decorations into editor', () => {
+ const spy = spyOn(
+ controller.decorationsController.editor.instance,
+ 'deltaDecorations',
+ );
+
+ controller.decorate({
+ data: { changes: computeDiff('123', '1234'), path: 'path' },
+ });
+
+ expect(spy).toHaveBeenCalledWith(
+ [],
+ [
+ {
+ range: new monaco.Range(1, 1, 1, 1),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
+ },
+ },
+ ],
+ );
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(controller.disposable, 'dispose').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('terminates worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled();
+ });
+
+ it('removes worker event listener', () => {
+ spyOn(
+ controller.dirtyDiffWorker,
+ 'removeEventListener',
+ ).and.callThrough();
+
+ controller.dispose();
+
+ expect(
+ controller.dirtyDiffWorker.removeEventListener,
+ ).toHaveBeenCalledWith('message', jasmine.anything());
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/diff/diff_spec.js b/spec/javascripts/ide/lib/diff/diff_spec.js
new file mode 100644
index 00000000000..57f3ac3d365
--- /dev/null
+++ b/spec/javascripts/ide/lib/diff/diff_spec.js
@@ -0,0 +1,80 @@
+import { computeDiff } from '~/ide/lib/diff/diff';
+
+describe('Multi-file editor library diff calculator', () => {
+ describe('computeDiff', () => {
+ it('returns empty array if no changes', () => {
+ const diff = computeDiff('123', '123');
+
+ expect(diff).toEqual([]);
+ });
+
+ describe('modified', () => {
+ it('', () => {
+ const diff = computeDiff('123', '1234')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ describe('added', () => {
+ it('', () => {
+ const diff = computeDiff('123', '123\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(3);
+ });
+ });
+
+ describe('removed', () => {
+ it('', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeTruthy();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeTruthy();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ it('includes line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.lineNumber).toBe(1);
+ });
+
+ it('includes end line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.endLineNumber).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/editor_options_spec.js b/spec/javascripts/ide/lib/editor_options_spec.js
new file mode 100644
index 00000000000..d149a883166
--- /dev/null
+++ b/spec/javascripts/ide/lib/editor_options_spec.js
@@ -0,0 +1,11 @@
+import editorOptions from '~/ide/lib/editor_options';
+
+describe('Multi-file editor library editor options', () => {
+ it('returns an array', () => {
+ expect(editorOptions).toEqual(jasmine.any(Array));
+ });
+
+ it('contains readOnly option', () => {
+ expect(editorOptions[0].readOnly).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
new file mode 100644
index 00000000000..2ccd87de1a7
--- /dev/null
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -0,0 +1,193 @@
+/* global monaco */
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import { file } from '../helpers';
+
+describe('Multi-file editor library', () => {
+ let instance;
+ let el;
+ let holder;
+
+ beforeEach(done => {
+ el = document.createElement('div');
+ holder = document.createElement('div');
+ el.appendChild(holder);
+
+ document.body.appendChild(el);
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = editor.create(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+
+ el.remove();
+ });
+
+ it('creates instance of editor', () => {
+ expect(editor.editorInstance).not.toBeNull();
+ });
+
+ it('creates instance returns cached instance', () => {
+ expect(editor.create(monaco)).toEqual(instance);
+ });
+
+ describe('createInstance', () => {
+ it('creates editor instance', () => {
+ spyOn(instance.monaco.editor, 'create').and.callThrough();
+
+ instance.createInstance(holder);
+
+ expect(instance.monaco.editor.create).toHaveBeenCalled();
+ });
+
+ it('creates dirty diff controller', () => {
+ instance.createInstance(holder);
+
+ expect(instance.dirtyDiffController).not.toBeNull();
+ });
+
+ it('creates model manager', () => {
+ instance.createInstance(holder);
+
+ expect(instance.modelManager).not.toBeNull();
+ });
+ });
+
+ describe('createDiffInstance', () => {
+ it('creates editor instance', () => {
+ spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough();
+
+ instance.createDiffInstance(holder);
+
+ expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith(holder, {
+ model: null,
+ contextmenu: true,
+ minimap: {
+ enabled: false,
+ },
+ readOnly: true,
+ scrollBeyondLastLine: false,
+ quickSuggestions: false,
+ occurrencesHighlight: false,
+ renderLineHighlight: 'none',
+ hideCursorInOverviewRuler: true,
+ wordWrap: 'bounded',
+ });
+ });
+ });
+
+ describe('createModel', () => {
+ it('calls model manager addModel', () => {
+ spyOn(instance.modelManager, 'addModel');
+
+ instance.createModel('FILE');
+
+ expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
+ });
+ });
+
+ describe('attachModel', () => {
+ let model;
+
+ beforeEach(() => {
+ instance.createInstance(document.createElement('div'));
+
+ model = instance.createModel(file());
+ });
+
+ it('sets the current model on the instance', () => {
+ instance.attachModel(model);
+
+ expect(instance.currentModel).toBe(model);
+ });
+
+ it('attaches the model to the current instance', () => {
+ spyOn(instance.instance, 'setModel');
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
+ });
+
+ it('sets original & modified when diff editor', () => {
+ spyOn(instance.instance, 'getEditorType').and.returnValue('vs.editor.IDiffEditor');
+ spyOn(instance.instance, 'setModel');
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith({
+ original: model.getOriginalModel(),
+ modified: model.getModel(),
+ });
+ });
+
+ it('attaches the model to the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'attachModel');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
+ });
+
+ it('re-decorates with the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'reDecorate');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
+ });
+ });
+
+ describe('clearEditor', () => {
+ it('resets the editor model', () => {
+ instance.createInstance(document.createElement('div'));
+
+ spyOn(instance.instance, 'setModel');
+
+ instance.clearEditor();
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposble dispose method', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('resets instance', () => {
+ instance.createInstance(document.createElement('div'));
+
+ expect(instance.instance).not.toBeNull();
+
+ instance.dispose();
+
+ expect(instance.instance).toBeNull();
+ });
+
+ it('does not dispose modelManager', () => {
+ spyOn(instance.modelManager, 'dispose');
+
+ instance.dispose();
+
+ expect(instance.modelManager.dispose).not.toHaveBeenCalled();
+ });
+
+ it('does not dispose decorationsController', () => {
+ spyOn(instance.decorationsController, 'dispose');
+
+ instance.dispose();
+
+ expect(instance.decorationsController.dispose).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/monaco_loader_spec.js b/spec/javascripts/ide/monaco_loader_spec.js
new file mode 100644
index 00000000000..7ab315aa8c8
--- /dev/null
+++ b/spec/javascripts/ide/monaco_loader_spec.js
@@ -0,0 +1,15 @@
+import monacoContext from 'monaco-editor/dev/vs/loader';
+import monacoLoader from '~/ide/monaco_loader';
+
+describe('MonacoLoader', () => {
+ it('calls require.config and exports require', () => {
+ expect(monacoContext.require.getConfig()).toEqual(
+ jasmine.objectContaining({
+ paths: {
+ vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
+ },
+ }),
+ );
+ expect(monacoLoader).toBe(monacoContext.require);
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
new file mode 100644
index 00000000000..5b7c8365641
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -0,0 +1,421 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import router from '~/ide/ide_router';
+import eventHub from '~/ide/eventhub';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store file actions', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('closeFile', () => {
+ let localFile;
+
+ beforeEach(() => {
+ localFile = file('testFile');
+ localFile.active = true;
+ localFile.opened = true;
+ localFile.parentTreeUrl = 'parentTreeUrl';
+
+ store.state.openFiles.push(localFile);
+ store.state.entries[localFile.path] = localFile;
+ });
+
+ it('closes open files', done => {
+ store
+ .dispatch('closeFile', localFile.path)
+ .then(() => {
+ expect(localFile.opened).toBeFalsy();
+ expect(localFile.active).toBeFalsy();
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('closes file even if file has changes', done => {
+ store.state.changedFiles.push(localFile);
+
+ store
+ .dispatch('closeFile', localFile.path)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+ expect(store.state.changedFiles.length).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('closes file & opens next available file', done => {
+ const f = {
+ ...file('newOpenFile'),
+ url: '/newOpenFile',
+ };
+
+ store.state.openFiles.push(f);
+ store.state.entries[f.path] = f;
+
+ store
+ .dispatch('closeFile', localFile.path)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(`/project${f.url}`);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('setFileActive', () => {
+ let localFile;
+ let scrollToTabSpy;
+ let oldScrollToTab;
+
+ beforeEach(() => {
+ scrollToTabSpy = jasmine.createSpy('scrollToTab');
+ oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
+ store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
+
+ localFile = file('setThisActive');
+
+ store.state.entries[localFile.path] = localFile;
+ });
+
+ afterEach(() => {
+ store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
+ });
+
+ it('calls scrollToTab', done => {
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(scrollToTabSpy).toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the file active', done => {
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns early if file is already active', done => {
+ localFile.active = true;
+
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(scrollToTabSpy).not.toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets current active file to not active', done => {
+ const f = file('newActive');
+ store.state.entries[f.path] = f;
+ localFile.active = true;
+ store.state.openFiles.push(localFile);
+
+ store
+ .dispatch('setFileActive', f.path)
+ .then(() => {
+ expect(localFile.active).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('resets location.hash for line highlighting', done => {
+ location.hash = 'test';
+
+ store
+ .dispatch('setFileActive', localFile.path)
+ .then(() => {
+ expect(location.hash).not.toBe('test');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('getFileData', () => {
+ let localFile;
+
+ beforeEach(() => {
+ spyOn(service, 'getFileData').and.returnValue(
+ Promise.resolve({
+ headers: {
+ 'page-title': 'testing getFileData',
+ },
+ json: () =>
+ Promise.resolve({
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ }),
+ }),
+ );
+
+ localFile = file(`newCreate-${Math.random()}`);
+ localFile.url = 'getFileDataURL';
+ store.state.entries[localFile.path] = localFile;
+ });
+
+ it('calls the service', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the file data', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(localFile.blamePath).toBe('blame_path');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets document title', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(document.title).toBe('testing getFileData');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the file as active', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds the file to open files', done => {
+ store
+ .dispatch('getFileData', localFile)
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(localFile.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('getRawFileData', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
+
+ tmpFile = file('tmpFile');
+ store.state.entries[tmpFile.path] = tmpFile;
+ });
+
+ it('calls getRawFileData service method', done => {
+ store
+ .dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('updates file raw data', done => {
+ store
+ .dispatch('getRawFileData', tmpFile)
+ .then(() => {
+ expect(tmpFile.raw).toBe('raw');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('changeFileContent', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ tmpFile = file('tmpFile');
+ store.state.entries[tmpFile.path] = tmpFile;
+ });
+
+ it('updates file content', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() => {
+ expect(tmpFile.content).toBe('content');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds file into changedFiles array', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds file once into changedFiles array', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() =>
+ store.dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content 123',
+ }),
+ )
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('removes file from changedFiles array if not changed', done => {
+ store
+ .dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: 'content',
+ })
+ .then(() =>
+ store.dispatch('changeFileContent', {
+ path: tmpFile.path,
+ content: '',
+ }),
+ )
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardFileChanges', () => {
+ let tmpFile;
+
+ beforeEach(() => {
+ spyOn(eventHub, '$on');
+
+ tmpFile = file();
+ tmpFile.content = 'testing';
+
+ store.state.changedFiles.push(tmpFile);
+ store.state.entries[tmpFile.path] = tmpFile;
+ });
+
+ it('resets file content', done => {
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(tmpFile.content).not.toBe('testing');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('removes file from changedFiles array', done => {
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('closes temp file', done => {
+ tmpFile.tempFile = true;
+ tmpFile.opened = true;
+
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(tmpFile.opened).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not re-open a closed temp file', done => {
+ tmpFile.tempFile = true;
+
+ expect(tmpFile.opened).toBeFalsy();
+
+ store
+ .dispatch('discardFileChanges', tmpFile.path)
+ .then(() => {
+ expect(tmpFile.opened).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
new file mode 100644
index 00000000000..381f038067b
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -0,0 +1,172 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../../helpers';
+
+describe('Multi-file store tree actions', () => {
+ let projectTree;
+
+ const basicCallParameters = {
+ endpoint: 'rootEndpoint',
+ projectId: 'abcproject',
+ branch: 'master',
+ branchId: 'master',
+ };
+
+ beforeEach(() => {
+ spyOn(router, 'push');
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: '',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('getFiles', () => {
+ beforeEach(() => {
+ spyOn(service, 'getFiles').and.returnValue(
+ Promise.resolve({
+ json: () =>
+ Promise.resolve([
+ 'file.txt',
+ 'folder/fileinfolder.js',
+ 'folder/subfolder/fileinsubfolder.js',
+ ]),
+ }),
+ );
+ });
+
+ it('calls service getFiles', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .then(() => {
+ expect(service.getFiles).toHaveBeenCalledWith('', 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds data into tree', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .then(() => {
+ projectTree = store.state.trees['abcproject/master'];
+ expect(projectTree.tree.length).toBe(2);
+ expect(projectTree.tree[0].type).toBe('tree');
+ expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
+ expect(projectTree.tree[1].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].name).toBe(
+ 'fileinsubfolder.js',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('toggleTreeOpen', () => {
+ let tree;
+
+ beforeEach(() => {
+ tree = file('testing', '1', 'tree');
+ store.state.entries[tree.path] = tree;
+ });
+
+ it('toggles the tree open', done => {
+ store
+ .dispatch('toggleTreeOpen', tree.path)
+ .then(() => {
+ expect(tree.opened).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('getLastCommitData', () => {
+ beforeEach(() => {
+ spyOn(service, 'getTreeLastCommit').and.returnValue(
+ Promise.resolve({
+ headers: {
+ 'more-logs-url': null,
+ },
+ json: () =>
+ Promise.resolve([
+ {
+ type: 'tree',
+ file_name: 'testing',
+ commit: {
+ message: 'commit message',
+ authored_date: '123',
+ },
+ },
+ ]),
+ }),
+ );
+
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ projectTree = store.state.trees['abcproject/mybranch'];
+ projectTree.tree.push(file('testing', '1', 'tree'));
+ projectTree.lastCommitPath = 'lastcommitpath';
+ });
+
+ it('calls service with lastCommitPath', done => {
+ store
+ .dispatch('getLastCommitData', projectTree)
+ .then(() => {
+ expect(service.getTreeLastCommit).toHaveBeenCalledWith(
+ 'lastcommitpath',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('updates trees last commit data', done => {
+ store
+ .dispatch('getLastCommitData', projectTree)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(projectTree.tree[0].lastCommit.message).toBe('commit message');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not update entry if not found', done => {
+ projectTree.tree[0].name = 'a';
+
+ store
+ .dispatch('getLastCommitData', projectTree)
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(projectTree.tree[0].lastCommit.message).not.toBe(
+ 'commit message',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
new file mode 100644
index 00000000000..cec572f4507
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -0,0 +1,306 @@
+import * as urlUtils from '~/lib/utils/url_utility';
+import store from '~/ide/stores';
+import router from '~/ide/ide_router';
+import { resetStore, file } from '../helpers';
+
+describe('Multi-file store actions', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('redirectToUrl', () => {
+ it('calls visitUrl', done => {
+ spyOn(urlUtils, 'visitUrl');
+
+ store
+ .dispatch('redirectToUrl', 'test')
+ .then(() => {
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith('test');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('setInitialData', () => {
+ it('commits initial data', done => {
+ store
+ .dispatch('setInitialData', { canCommit: true })
+ .then(() => {
+ expect(store.state.canCommit).toBeTruthy();
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardAllChanges', () => {
+ beforeEach(() => {
+ const f = file('discardAll');
+ f.changed = true;
+
+ store.state.openFiles.push(f);
+ store.state.changedFiles.push(f);
+ store.state.entries[f.path] = f;
+ });
+
+ it('discards changes in file', done => {
+ store
+ .dispatch('discardAllChanges')
+ .then(() => {
+ expect(store.state.openFiles.changed).toBeFalsy();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('removes all files from changedFiles state', done => {
+ store
+ .dispatch('discardAllChanges')
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+ expect(store.state.openFiles.length).toBe(1);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('closeAllFiles', () => {
+ beforeEach(() => {
+ const f = file('closeAll');
+ store.state.openFiles.push(f);
+ store.state.openFiles[0].opened = true;
+ store.state.entries[f.path] = f;
+ });
+
+ it('closes all open files', done => {
+ store
+ .dispatch('closeAllFiles')
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('createTempEntry', () => {
+ beforeEach(() => {
+ document.body.innerHTML += '<div class="flash-container"></div>';
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'mybranch';
+
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ store.state.projects.abcproject = {
+ web_url: '',
+ };
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ describe('tree', () => {
+ it('creates temp tree', done => {
+ store
+ .dispatch('createTempEntry', {
+ branchId: store.state.currentBranchId,
+ name: 'test',
+ type: 'tree',
+ })
+ .then(() => {
+ const entry = store.state.entries.test;
+
+ expect(entry).not.toBeNull();
+ expect(entry.type).toBe('tree');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('creates new folder inside another tree', done => {
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ path: 'testing',
+ tree: [],
+ };
+
+ store.state.entries[tree.path] = tree;
+
+ store
+ .dispatch('createTempEntry', {
+ branchId: store.state.currentBranchId,
+ name: 'testing/test',
+ type: 'tree',
+ })
+ .then(() => {
+ expect(tree.tree[0].tempFile).toBeTruthy();
+ expect(tree.tree[0].name).toBe('test');
+ expect(tree.tree[0].type).toBe('tree');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not create new tree if already exists', done => {
+ const tree = {
+ type: 'tree',
+ path: 'testing',
+ tempFile: false,
+ tree: [],
+ };
+
+ store.state.entries[tree.path] = tree;
+
+ store
+ .dispatch('createTempEntry', {
+ branchId: store.state.currentBranchId,
+ name: 'testing',
+ type: 'tree',
+ })
+ .then(() => {
+ expect(store.state.entries[tree.path].tempFile).toEqual(false);
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('blob', () => {
+ it('creates temp file', done => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ expect(f.tempFile).toBeTruthy();
+ expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(
+ 1,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds tmp file to open files', done => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(f.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds tmp file to changed files', done => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ expect(store.state.changedFiles.length).toBe(1);
+ expect(store.state.changedFiles[0].name).toBe(f.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets tmp file as active', done => {
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(f => {
+ expect(f.active).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('creates flash message if file already exists', done => {
+ const f = file('test', '1', 'blob');
+ store.state.trees['abcproject/mybranch'].tree = [f];
+ store.state.entries[f.path] = f;
+
+ store
+ .dispatch('createTempEntry', {
+ name: 'test',
+ branchId: 'mybranch',
+ type: 'blob',
+ })
+ .then(() => {
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('popHistoryState', () => {});
+
+ describe('scrollToTab', () => {
+ it('focuses the current active element', done => {
+ document.body.innerHTML +=
+ '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>';
+ const el = document.querySelector('.repo-tab');
+ spyOn(el, 'focus');
+
+ store
+ .dispatch('scrollToTab')
+ .then(() => {
+ setTimeout(() => {
+ expect(el.focus).toHaveBeenCalled();
+
+ document.getElementById('tabs').remove();
+
+ done();
+ });
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateViewer', () => {
+ it('updates viewer state', done => {
+ store
+ .dispatch('updateViewer', 'diff')
+ .then(() => {
+ expect(store.state.viewer).toBe('diff');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
new file mode 100644
index 00000000000..a613f3a21cc
--- /dev/null
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -0,0 +1,55 @@
+import * as getters from '~/ide/stores/getters';
+import state from '~/ide/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store getters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('activeFile', () => {
+ it('returns the current active file', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+ localState.openFiles[1].active = true;
+
+ expect(getters.activeFile(localState).name).toBe('active');
+ });
+
+ it('returns undefined if no active files are found', () => {
+ localState.openFiles.push(file());
+ localState.openFiles.push(file('active'));
+
+ expect(getters.activeFile(localState)).toBeNull();
+ });
+ });
+
+ describe('modifiedFiles', () => {
+ it('returns a list of modified files', () => {
+ localState.openFiles.push(file());
+ localState.changedFiles.push(file('changed'));
+ localState.changedFiles[0].changed = true;
+
+ const modifiedFiles = getters.modifiedFiles(localState);
+
+ expect(modifiedFiles.length).toBe(1);
+ expect(modifiedFiles[0].name).toBe('changed');
+ });
+ });
+
+ describe('addedFiles', () => {
+ it('returns a list of added files', () => {
+ localState.openFiles.push(file());
+ localState.changedFiles.push(file('added'));
+ localState.changedFiles[0].changed = true;
+ localState.changedFiles[0].tempFile = true;
+
+ const modifiedFiles = getters.addedFiles(localState);
+
+ expect(modifiedFiles.length).toBe(1);
+ expect(modifiedFiles[0].name).toBe('added');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
new file mode 100644
index 00000000000..90ded940227
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -0,0 +1,505 @@
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import router from '~/ide/ide_router';
+import * as urlUtils from '~/lib/utils/url_utility';
+import eventHub from '~/ide/eventhub';
+import * as consts from '~/ide/stores/modules/commit/constants';
+import { resetStore, file } from 'spec/ide/helpers';
+
+describe('IDE commit module actions', () => {
+ beforeEach(() => {
+ spyOn(router, 'push');
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ describe('updateCommitMessage', () => {
+ it('updates store with new commit message', done => {
+ store
+ .dispatch('commit/updateCommitMessage', 'testing')
+ .then(() => {
+ expect(store.state.commit.commitMessage).toBe('testing');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('discardDraft', () => {
+ it('resets commit message to blank', done => {
+ store.state.commit.commitMessage = 'testing';
+
+ store
+ .dispatch('commit/discardDraft')
+ .then(() => {
+ expect(store.state.commit.commitMessage).not.toBe('testing');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateCommitAction', () => {
+ it('updates store with new commit action', done => {
+ store
+ .dispatch('commit/updateCommitAction', '1')
+ .then(() => {
+ expect(store.state.commit.commitAction).toBe('1');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateBranchName', () => {
+ it('updates store with new branch name', done => {
+ store
+ .dispatch('commit/updateBranchName', 'branch-name')
+ .then(() => {
+ expect(store.state.commit.newBranchName).toBe('branch-name');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('setLastCommitMessage', () => {
+ beforeEach(() => {
+ Object.assign(store.state, {
+ currentProjectId: 'abcproject',
+ projects: {
+ abcproject: {
+ web_url: 'http://testing',
+ },
+ },
+ });
+ });
+
+ it('updates commit message with short_id', done => {
+ store
+ .dispatch('commit/setLastCommitMessage', { short_id: '123' })
+ .then(() => {
+ expect(store.state.lastCommitMsg).toContain(
+ 'Your changes have been committed. Commit <a href="http://testing/commit/123" class="commit-sha">123</a>',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates commit message with stats', done => {
+ store
+ .dispatch('commit/setLastCommitMessage', {
+ short_id: '123',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ })
+ .then(() => {
+ expect(store.state.lastCommitMsg).toBe(
+ 'Your changes have been committed. Commit <a href="http://testing/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('checkCommitStatus', () => {
+ beforeEach(() => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+ });
+
+ it('calls service', done => {
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '123' },
+ },
+ }),
+ );
+
+ store
+ .dispatch('commit/checkCommitStatus')
+ .then(() => {
+ expect(service.getBranchData).toHaveBeenCalledWith(
+ 'abcproject',
+ 'master',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns true if current ref does not equal returned ID', done => {
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '123' },
+ },
+ }),
+ );
+
+ store
+ .dispatch('commit/checkCommitStatus')
+ .then(val => {
+ expect(val).toBeTruthy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns false if current ref equals returned ID', done => {
+ spyOn(service, 'getBranchData').and.returnValue(
+ Promise.resolve({
+ data: {
+ commit: { id: '1' },
+ },
+ }),
+ );
+
+ store
+ .dispatch('commit/checkCommitStatus')
+ .then(val => {
+ expect(val).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateFilesAfterCommit', () => {
+ const data = {
+ id: '123',
+ message: 'testing commit message',
+ committed_date: '123',
+ committer_name: 'root',
+ };
+ const branch = 'master';
+ let f;
+
+ beforeEach(() => {
+ spyOn(eventHub, '$emit');
+
+ f = file('changedFile');
+ Object.assign(f, {
+ active: true,
+ changed: true,
+ content: 'file content',
+ });
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: 'web_url',
+ branches: {
+ master: {
+ workingReference: '',
+ },
+ },
+ };
+ store.state.changedFiles.push(f, {
+ ...file('changedFile2'),
+ changed: true,
+ });
+ store.state.openFiles = store.state.changedFiles;
+
+ store.state.changedFiles.forEach(changedFile => {
+ store.state.entries[changedFile.path] = changedFile;
+ });
+ });
+
+ it('updates stores working reference', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(
+ store.state.projects.abcproject.branches.master.workingReference,
+ ).toBe(data.id);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('resets all files changed status', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ store.state.openFiles.forEach(entry => {
+ expect(entry.changed).toBeFalsy();
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('removes all changed files', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(store.state.changedFiles.length).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('sets files commit data', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(f.lastCommit.message).toBe(data.message);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates raw content for changed file', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(f.raw).toBe(f.content);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('emits changed event for file', done => {
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ `editor.update.model.content.${f.path}`,
+ f.content,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('pushes route to new branch if commitAction is new branch', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(
+ `/project/abcproject/blob/master/${f.path}`,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('resets stores commit actions', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+
+ store
+ .dispatch('commit/updateFilesAfterCommit', {
+ data,
+ branch,
+ })
+ .then(() => {
+ expect(store.state.commit.commitAction).not.toBe(
+ consts.COMMIT_TO_NEW_BRANCH,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('commitChanges', () => {
+ beforeEach(() => {
+ spyOn(urlUtils, 'visitUrl');
+
+ document.body.innerHTML += '<div class="flash-container"></div>';
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: 'webUrl',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+ store.state.changedFiles.push(file('changed'));
+ store.state.changedFiles[0].active = true;
+ store.state.openFiles = store.state.changedFiles;
+
+ store.state.openFiles.forEach(f => {
+ store.state.entries[f.path] = f;
+ });
+
+ store.state.commit.commitAction = '2';
+ store.state.commit.commitMessage = 'testing 123';
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(
+ Promise.resolve({
+ data: {
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ },
+ }),
+ );
+ });
+
+ it('calls service', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(service.commit).toHaveBeenCalledWith('abcproject', {
+ branch: jasmine.anything(),
+ commit_message: 'testing 123',
+ actions: [
+ {
+ action: 'update',
+ file_path: jasmine.anything(),
+ content: jasmine.anything(),
+ encoding: jasmine.anything(),
+ },
+ ],
+ start_branch: 'master',
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('pushes router to new route', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(router.push).toHaveBeenCalledWith(
+ `/project/${store.state.currentProjectId}/blob/${
+ store.getters['commit/newBranchName']
+ }/changed`,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets last Commit Msg', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.lastCommitMsg).toBe(
+ 'Your changes have been committed. Commit <a href="webUrl/commit/123" class="commit-sha">123</a> with 1 additions, 2 deletions.',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds commit data to changed files', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.state.openFiles[0].lastCommit.message).toBe(
+ 'test message',
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('redirects to new merge request page', done => {
+ spyOn(eventHub, '$on');
+
+ store.state.commit.commitAction = '3';
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith(
+ `webUrl/merge_requests/new?merge_request[source_branch]=${
+ store.getters['commit/newBranchName']
+ }&merge_request[target_branch]=master`,
+ );
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('failed', () => {
+ beforeEach(() => {
+ spyOn(service, 'commit').and.returnValue(
+ Promise.resolve({
+ data: {
+ message: 'failed message',
+ },
+ }),
+ );
+ });
+
+ it('shows failed message', done => {
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ const alert = document.querySelector('.flash-container');
+
+ expect(alert.textContent.trim()).toBe('failed message');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
new file mode 100644
index 00000000000..e396284ec2c
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -0,0 +1,128 @@
+import commitState from '~/ide/stores/modules/commit/state';
+import * as consts from '~/ide/stores/modules/commit/constants';
+import * as getters from '~/ide/stores/modules/commit/getters';
+
+describe('IDE commit module getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = commitState();
+ });
+
+ describe('discardDraftButtonDisabled', () => {
+ it('returns true when commitMessage is empty', () => {
+ expect(getters.discardDraftButtonDisabled(state)).toBeTruthy();
+ });
+
+ it('returns false when commitMessage is not empty & loading is false', () => {
+ state.commitMessage = 'test';
+ state.submitCommitLoading = false;
+
+ expect(getters.discardDraftButtonDisabled(state)).toBeFalsy();
+ });
+
+ it('returns true when commitMessage is not empty & loading is true', () => {
+ state.commitMessage = 'test';
+ state.submitCommitLoading = true;
+
+ expect(getters.discardDraftButtonDisabled(state)).toBeTruthy();
+ });
+ });
+
+ describe('commitButtonDisabled', () => {
+ const localGetters = {
+ discardDraftButtonDisabled: false,
+ };
+ const rootState = {
+ changedFiles: ['a'],
+ };
+
+ it('returns false when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeFalsy();
+ });
+
+ it('returns true when discardDraftButtonDisabled is false & changedFiles is empty', () => {
+ rootState.changedFiles.length = 0;
+
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeTruthy();
+ });
+
+ it('returns true when discardDraftButtonDisabled is true', () => {
+ localGetters.discardDraftButtonDisabled = true;
+
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeTruthy();
+ });
+
+ it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
+ localGetters.discardDraftButtonDisabled = false;
+ rootState.changedFiles.length = 0;
+
+ expect(
+ getters.commitButtonDisabled(state, localGetters, rootState),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('newBranchName', () => {
+ it('includes username, currentBranchId, patch & random number', () => {
+ gon.current_username = 'username';
+
+ const branch = getters.newBranchName(state, null, {
+ currentBranchId: 'testing',
+ });
+
+ expect(branch).toMatch(/username-testing-patch-\d{5}$/);
+ });
+ });
+
+ describe('branchName', () => {
+ const rootState = {
+ currentBranchId: 'master',
+ };
+ const localGetters = {
+ newBranchName: 'newBranchName',
+ };
+
+ beforeEach(() => {
+ Object.assign(state, {
+ newBranchName: 'state-newBranchName',
+ });
+ });
+
+ it('defualts to currentBranchId', () => {
+ expect(getters.branchName(state, null, rootState)).toBe('master');
+ });
+
+ ['COMMIT_TO_NEW_BRANCH', 'COMMIT_TO_NEW_BRANCH_MR'].forEach(type => {
+ describe(type, () => {
+ beforeEach(() => {
+ Object.assign(state, {
+ commitAction: consts[type],
+ });
+ });
+
+ it('uses newBranchName when not empty', () => {
+ expect(getters.branchName(state, localGetters, rootState)).toBe(
+ 'state-newBranchName',
+ );
+ });
+
+ it('uses getters newBranchName when state newBranchName is empty', () => {
+ Object.assign(state, {
+ newBranchName: '',
+ });
+
+ expect(getters.branchName(state, localGetters, rootState)).toBe(
+ 'newBranchName',
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/commit/mutations_spec.js b/spec/javascripts/ide/stores/modules/commit/mutations_spec.js
new file mode 100644
index 00000000000..5de7a281d34
--- /dev/null
+++ b/spec/javascripts/ide/stores/modules/commit/mutations_spec.js
@@ -0,0 +1,42 @@
+import commitState from '~/ide/stores/modules/commit/state';
+import mutations from '~/ide/stores/modules/commit/mutations';
+
+describe('IDE commit module mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = commitState();
+ });
+
+ describe('UPDATE_COMMIT_MESSAGE', () => {
+ it('updates commitMessage', () => {
+ mutations.UPDATE_COMMIT_MESSAGE(state, 'testing');
+
+ expect(state.commitMessage).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_COMMIT_ACTION', () => {
+ it('updates commitAction', () => {
+ mutations.UPDATE_COMMIT_ACTION(state, 'testing');
+
+ expect(state.commitAction).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_NEW_BRANCH_NAME', () => {
+ it('updates newBranchName', () => {
+ mutations.UPDATE_NEW_BRANCH_NAME(state, 'testing');
+
+ expect(state.newBranchName).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_LOADING', () => {
+ it('updates submitCommitLoading', () => {
+ mutations.UPDATE_LOADING(state, true);
+
+ expect(state.submitCommitLoading).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js
new file mode 100644
index 00000000000..a7167537ef2
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/branch_spec.js
@@ -0,0 +1,18 @@
+import mutations from '~/ide/stores/mutations/branch';
+import state from '~/ide/stores/state';
+
+describe('Multi-file store branch mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('SET_CURRENT_BRANCH', () => {
+ it('sets currentBranch', () => {
+ mutations.SET_CURRENT_BRANCH(localState, 'master');
+
+ expect(localState.currentBranchId).toBe('master');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
new file mode 100644
index 00000000000..131380248e8
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -0,0 +1,157 @@
+import mutations from '~/ide/stores/mutations/file';
+import state from '~/ide/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store file mutations', () => {
+ let localState;
+ let localFile;
+
+ beforeEach(() => {
+ localState = state();
+ localFile = file();
+
+ localState.entries[localFile.path] = localFile;
+ });
+
+ describe('SET_FILE_ACTIVE', () => {
+ it('sets the file active', () => {
+ mutations.SET_FILE_ACTIVE(localState, {
+ path: localFile.path,
+ active: true,
+ });
+
+ expect(localFile.active).toBeTruthy();
+ });
+ });
+
+ describe('TOGGLE_FILE_OPEN', () => {
+ beforeEach(() => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
+ });
+
+ it('adds into opened files', () => {
+ expect(localFile.opened).toBeTruthy();
+ expect(localState.openFiles.length).toBe(1);
+ });
+
+ it('removes from opened files', () => {
+ mutations.TOGGLE_FILE_OPEN(localState, localFile.path);
+
+ expect(localFile.opened).toBeFalsy();
+ expect(localState.openFiles.length).toBe(0);
+ });
+ });
+
+ describe('SET_FILE_DATA', () => {
+ it('sets extra file data', () => {
+ mutations.SET_FILE_DATA(localState, {
+ data: {
+ blame_path: 'blame',
+ commits_path: 'commits',
+ permalink: 'permalink',
+ raw_path: 'raw',
+ binary: true,
+ render_error: 'render_error',
+ },
+ file: localFile,
+ });
+
+ expect(localFile.blamePath).toBe('blame');
+ expect(localFile.commitsPath).toBe('commits');
+ expect(localFile.permalink).toBe('permalink');
+ expect(localFile.rawPath).toBe('raw');
+ expect(localFile.binary).toBeTruthy();
+ expect(localFile.renderError).toBe('render_error');
+ });
+ });
+
+ describe('SET_FILE_RAW_DATA', () => {
+ it('sets raw data', () => {
+ mutations.SET_FILE_RAW_DATA(localState, {
+ file: localFile,
+ raw: 'testing',
+ });
+
+ expect(localFile.raw).toBe('testing');
+ });
+ });
+
+ describe('UPDATE_FILE_CONTENT', () => {
+ beforeEach(() => {
+ localFile.raw = 'test';
+ });
+
+ it('sets content', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ path: localFile.path,
+ content: 'test',
+ });
+
+ expect(localFile.content).toBe('test');
+ });
+
+ it('sets changed if content does not match raw', () => {
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ path: localFile.path,
+ content: 'testing',
+ });
+
+ expect(localFile.content).toBe('testing');
+ expect(localFile.changed).toBeTruthy();
+ });
+
+ it('sets changed if file is a temp file', () => {
+ localFile.tempFile = true;
+
+ mutations.UPDATE_FILE_CONTENT(localState, {
+ path: localFile.path,
+ content: '',
+ });
+
+ expect(localFile.changed).toBeTruthy();
+ });
+ });
+
+ describe('DISCARD_FILE_CHANGES', () => {
+ beforeEach(() => {
+ localFile.content = 'test';
+ localFile.changed = true;
+ });
+
+ it('resets content and changed', () => {
+ mutations.DISCARD_FILE_CHANGES(localState, localFile.path);
+
+ expect(localFile.content).toBe('');
+ expect(localFile.changed).toBeFalsy();
+ });
+ });
+
+ describe('ADD_FILE_TO_CHANGED', () => {
+ it('adds file into changed files array', () => {
+ mutations.ADD_FILE_TO_CHANGED(localState, localFile.path);
+
+ expect(localState.changedFiles.length).toBe(1);
+ });
+ });
+
+ describe('REMOVE_FILE_FROM_CHANGED', () => {
+ it('removes files from changed files array', () => {
+ localState.changedFiles.push(localFile);
+
+ mutations.REMOVE_FILE_FROM_CHANGED(localState, localFile.path);
+
+ expect(localState.changedFiles.length).toBe(0);
+ });
+ });
+
+ describe('TOGGLE_FILE_CHANGED', () => {
+ it('updates file changed status', () => {
+ mutations.TOGGLE_FILE_CHANGED(localState, {
+ file: localFile,
+ changed: true,
+ });
+
+ expect(localFile.changed).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js
new file mode 100644
index 00000000000..e6c085eaff6
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/tree_spec.js
@@ -0,0 +1,69 @@
+import mutations from '~/ide/stores/mutations/tree';
+import state from '~/ide/stores/state';
+import { file } from '../../helpers';
+
+describe('Multi-file store tree mutations', () => {
+ let localState;
+ let localTree;
+
+ beforeEach(() => {
+ localState = state();
+ localTree = file();
+
+ localState.entries[localTree.path] = localTree;
+ });
+
+ describe('TOGGLE_TREE_OPEN', () => {
+ it('toggles tree open', () => {
+ mutations.TOGGLE_TREE_OPEN(localState, localTree.path);
+
+ expect(localTree.opened).toBeTruthy();
+
+ mutations.TOGGLE_TREE_OPEN(localState, localTree.path);
+
+ expect(localTree.opened).toBeFalsy();
+ });
+ });
+
+ describe('SET_DIRECTORY_DATA', () => {
+ const data = [
+ {
+ name: 'tree',
+ },
+ {
+ name: 'submodule',
+ },
+ {
+ name: 'blob',
+ },
+ ];
+
+ it('adds directory data', () => {
+ localState.trees['project/master'] = {
+ tree: [],
+ };
+
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ treePath: 'project/master',
+ });
+
+ const tree = localState.trees['project/master'];
+
+ expect(tree.tree.length).toBe(3);
+ expect(tree.tree[0].name).toBe('tree');
+ expect(tree.tree[1].name).toBe('submodule');
+ expect(tree.tree[2].name).toBe('blob');
+ });
+ });
+
+ describe('REMOVE_ALL_CHANGES_FILES', () => {
+ it('removes all files from changedFiles state', () => {
+ localState.changedFiles.push(file('REMOVE_ALL_CHANGES_FILES'));
+
+ mutations.REMOVE_ALL_CHANGES_FILES(localState);
+
+ expect(localState.changedFiles.length).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
new file mode 100644
index 00000000000..38162a470ad
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -0,0 +1,79 @@
+import mutations from '~/ide/stores/mutations';
+import state from '~/ide/stores/state';
+import { file } from '../helpers';
+
+describe('Multi-file store mutations', () => {
+ let localState;
+ let entry;
+
+ beforeEach(() => {
+ localState = state();
+ entry = file();
+
+ localState.entries[entry.path] = entry;
+ });
+
+ describe('SET_INITIAL_DATA', () => {
+ it('sets all initial data', () => {
+ mutations.SET_INITIAL_DATA(localState, {
+ test: 'test',
+ });
+
+ expect(localState.test).toBe('test');
+ });
+ });
+
+ describe('TOGGLE_LOADING', () => {
+ it('toggles loading of entry', () => {
+ mutations.TOGGLE_LOADING(localState, { entry });
+
+ expect(entry.loading).toBeTruthy();
+
+ mutations.TOGGLE_LOADING(localState, { entry });
+
+ expect(entry.loading).toBeFalsy();
+ });
+
+ it('toggles loading of entry and sets specific value', () => {
+ mutations.TOGGLE_LOADING(localState, { entry });
+
+ expect(entry.loading).toBeTruthy();
+
+ mutations.TOGGLE_LOADING(localState, { entry, forceValue: true });
+
+ expect(entry.loading).toBeTruthy();
+ });
+ });
+
+ describe('SET_LEFT_PANEL_COLLAPSED', () => {
+ it('sets left panel collapsed', () => {
+ mutations.SET_LEFT_PANEL_COLLAPSED(localState, true);
+
+ expect(localState.leftPanelCollapsed).toBeTruthy();
+
+ mutations.SET_LEFT_PANEL_COLLAPSED(localState, false);
+
+ expect(localState.leftPanelCollapsed).toBeFalsy();
+ });
+ });
+
+ describe('SET_RIGHT_PANEL_COLLAPSED', () => {
+ it('sets right panel collapsed', () => {
+ mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true);
+
+ expect(localState.rightPanelCollapsed).toBeTruthy();
+
+ mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false);
+
+ expect(localState.rightPanelCollapsed).toBeFalsy();
+ });
+ });
+
+ describe('UPDATE_VIEWER', () => {
+ it('sets viewer state', () => {
+ mutations.UPDATE_VIEWER(localState, 'diff');
+
+ expect(localState.viewer).toBe('diff');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
new file mode 100644
index 00000000000..f38ac6dd82f
--- /dev/null
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -0,0 +1,66 @@
+import * as utils from '~/ide/stores/utils';
+
+describe('Multi-file store utils', () => {
+ describe('setPageTitle', () => {
+ it('sets the document page title', () => {
+ utils.setPageTitle('test');
+
+ expect(document.title).toBe('test');
+ });
+ });
+
+ describe('findIndexOfFile', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = [
+ {
+ path: '1',
+ },
+ {
+ path: '2',
+ },
+ ];
+ });
+
+ it('finds in the index of an entry by path', () => {
+ const index = utils.findIndexOfFile(localState, {
+ path: '2',
+ });
+
+ expect(index).toBe(1);
+ });
+ });
+
+ describe('findEntry', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = {
+ tree: [
+ {
+ type: 'tree',
+ name: 'test',
+ },
+ {
+ type: 'blob',
+ name: 'file',
+ },
+ ],
+ };
+ });
+
+ it('returns an entry found by name', () => {
+ const foundEntry = utils.findEntry(localState.tree, 'tree', 'test');
+
+ expect(foundEntry.type).toBe('tree');
+ expect(foundEntry.name).toBe('test');
+ });
+
+ it('returns undefined when no entry found', () => {
+ const foundEntry = utils.findEntry(localState.tree, 'blob', 'test');
+
+ expect(foundEntry).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 584db6c6632..d5a87b5ce20 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -1,8 +1,7 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import '~/render_math';
-import '~/render_gfm';
+import '~/behaviors/markdown/render_gfm';
import * as urlUtils from '~/lib/utils/url_utility';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js
index a95a7e2a5be..ca0e7c395a0 100644
--- a/spec/javascripts/lib/utils/text_markdown_spec.js
+++ b/spec/javascripts/lib/utils/text_markdown_spec.js
@@ -1,4 +1,4 @@
-import textUtils from '~/lib/utils/text_markdown';
+import { insertMarkdownText } from '~/lib/utils/text_markdown';
describe('init markdown', () => {
let textArea;
@@ -21,7 +21,7 @@ describe('init markdown', () => {
textArea.selectionStart = 0;
textArea.selectionEnd = 0;
- textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+ insertMarkdownText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
@@ -32,7 +32,7 @@ describe('init markdown', () => {
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
- textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+ insertMarkdownText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}\n* `);
});
@@ -43,7 +43,7 @@ describe('init markdown', () => {
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
- textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+ insertMarkdownText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
@@ -54,7 +54,7 @@ describe('init markdown', () => {
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
- textUtils.insertText(textArea, textArea.value, '*', null, '', false);
+ insertMarkdownText(textArea, textArea.value, '*', null, '', false);
expect(textArea.value).toEqual(`${initialValue}* `);
});
diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js
index eb644e698da..dc9dc4d4249 100644
--- a/spec/javascripts/merge_request_notes_spec.js
+++ b/spec/javascripts/merge_request_notes_spec.js
@@ -3,8 +3,7 @@ import _ from 'underscore';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
-import '~/render_gfm';
-import '~/render_math';
+import '~/behaviors/markdown/render_gfm';
import Notes from '~/notes';
const upArrowKeyCode = 38;
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 29b355307ef..eba6dcf47c5 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -18,6 +18,7 @@ describe('Dashboard', () => {
deploymentEndpoint: null,
emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
emptyLoadingSvgPath: '/path/to/loading.svg',
+ emptyNoDataSvgPath: '/path/to/no-data.svg',
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
};
diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js
index df3198dd3e2..b4c5f4baa78 100644
--- a/spec/javascripts/monitoring/dashboard_state_spec.js
+++ b/spec/javascripts/monitoring/dashboard_state_spec.js
@@ -2,13 +2,22 @@ import Vue from 'vue';
import EmptyState from '~/monitoring/components/empty_state.vue';
import { statePaths } from './mock_data';
-const createComponent = (propsData) => {
+function createComponent(props) {
const Component = Vue.extend(EmptyState);
return new Component({
- propsData,
+ propsData: {
+ ...props,
+ settingsPath: statePaths.settingsPath,
+ clustersPath: statePaths.clustersPath,
+ documentationPath: statePaths.documentationPath,
+ emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
+ emptyLoadingSvgPath: '/path/to/loading.svg',
+ emptyNoDataSvgPath: '/path/to/no-data.svg',
+ emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
+ },
}).$mount();
-};
+}
function getTextFromNode(component, selector) {
return component.$el.querySelector(selector).firstChild.nodeValue.trim();
@@ -19,11 +28,6 @@ describe('EmptyState', () => {
it('currentState', () => {
const component = createComponent({
selectedState: 'gettingStarted',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.currentState).toBe(component.states.gettingStarted);
@@ -32,11 +36,6 @@ describe('EmptyState', () => {
it('showButtonDescription returns a description with a link for the unableToConnect state', () => {
const component = createComponent({
selectedState: 'unableToConnect',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.showButtonDescription).toEqual(true);
@@ -45,11 +44,6 @@ describe('EmptyState', () => {
it('showButtonDescription returns the description without a link for any other state', () => {
const component = createComponent({
selectedState: 'loading',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.showButtonDescription).toEqual(false);
@@ -59,12 +53,6 @@ describe('EmptyState', () => {
it('should show the gettingStarted state', () => {
const component = createComponent({
selectedState: 'gettingStarted',
- settingsPath: statePaths.settingsPath,
- clustersPath: statePaths.clustersPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.$el.querySelector('svg')).toBeDefined();
@@ -76,11 +64,6 @@ describe('EmptyState', () => {
it('should show the loading state', () => {
const component = createComponent({
selectedState: 'loading',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.$el.querySelector('svg')).toBeDefined();
@@ -92,11 +75,6 @@ describe('EmptyState', () => {
it('should show the unableToConnect state', () => {
const component = createComponent({
selectedState: 'unableToConnect',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- emptyGettingStartedSvgPath: 'foo',
- emptyLoadingSvgPath: 'foo',
- emptyUnableToConnectSvgPath: 'foo',
});
expect(component.$el.querySelector('svg')).toBeDefined();
diff --git a/spec/javascripts/notes/components/diff_file_header_spec.js b/spec/javascripts/notes/components/diff_file_header_spec.js
index aed30a087a6..ef6d513444a 100644
--- a/spec/javascripts/notes/components/diff_file_header_spec.js
+++ b/spec/javascripts/notes/components/diff_file_header_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import DiffFileHeader from '~/notes/components/diff_file_header.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const discussionFixture = 'merge_requests/diff_discussion.json';
diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js
index 7f1f4bf0bcd..f4ec7132dbd 100644
--- a/spec/javascripts/notes/components/diff_with_note_spec.js
+++ b/spec/javascripts/notes/components/diff_with_note_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import DiffWithNote from '~/notes/components/diff_with_note.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const discussionFixture = 'merge_requests/diff_discussion.json';
const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index ac39418c3e6..0e792eee5e9 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -3,7 +3,7 @@ import _ from 'underscore';
import Vue from 'vue';
import notesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service';
-import '~/render_gfm';
+import '~/behaviors/markdown/render_gfm';
import * as mockData from '../mock_data';
const vueMatchers = {
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index ba0a70bed17..ec56ab0e2f0 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -7,7 +7,7 @@ import * as urlUtils from '~/lib/utils/url_utility';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
-import '~/render_gfm';
+import '~/behaviors/markdown/render_gfm';
import Notes from '~/notes';
import timeoutPromise from './helpers/set_timeout_promise_helper';
@@ -16,15 +16,15 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
window.gl = window.gl || {};
gl.utils = gl.utils || {};
- const htmlEscape = (comment) => {
- const escapedString = comment.replace(/["&'<>]/g, (a) => {
+ const htmlEscape = comment => {
+ const escapedString = comment.replace(/["&'<>]/g, a => {
const escapedToken = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
- '`': '&#x60;'
+ '`': '&#x60;',
}[a];
return escapedToken;
@@ -39,7 +39,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw';
preloadFixtures(commentsTemplate);
- beforeEach(function () {
+ beforeEach(function() {
loadFixtures(commentsTemplate);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
@@ -51,6 +51,17 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$('body').removeAttr('data-page');
});
+ describe('addBinding', () => {
+ it('calls postComment when comment button is clicked', () => {
+ spyOn(Notes.prototype, 'postComment');
+ this.notes = new Notes('', []);
+
+ $('.js-comment-button').click();
+
+ expect(Notes.prototype.postComment).toHaveBeenCalled();
+ });
+ });
+
describe('task lists', function() {
let mock;
@@ -58,7 +69,13 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
spyOn(axios, 'patch').and.callThrough();
mock = new MockAdapter(axios);
- mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {});
+ mock
+ .onPatch(
+ `${
+ gl.TEST_HOST
+ }/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
+ )
+ .reply(200, {});
$('.js-comment-button').on('click', function(e) {
e.preventDefault();
@@ -73,18 +90,27 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('modifies the Markdown field', function() {
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
- $('input[type=checkbox]').attr('checked', true)[1].dispatchEvent(changeEvent);
+ $('input[type=checkbox]')
+ .attr('checked', true)[1]
+ .dispatchEvent(changeEvent);
- expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item');
+ expect($('.js-task-list-field.original-task-list').val()).toBe(
+ '- [x] Task List Item',
+ );
});
it('submits an ajax request on tasklist:changed', function(done) {
$('.js-task-list-container').trigger('tasklist:changed');
setTimeout(() => {
- expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, {
- note: { note: '' },
- });
+ expect(axios.patch).toHaveBeenCalledWith(
+ `${
+ gl.TEST_HOST
+ }/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
+ {
+ note: { note: '' },
+ },
+ );
done();
});
});
@@ -100,10 +126,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
spyOn(this.notes, 'renderNote').and.stub();
$(textarea).data('autosave', {
- reset: function() {}
+ reset: function() {},
});
- $('.js-comment-button').on('click', (e) => {
+ $('.js-comment-button').on('click', e => {
const $form = $(this);
e.preventDefault();
this.notes.addNote($form);
@@ -149,7 +175,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
<div class="note-text">${sampleComment}</div>
</li>`,
note: sampleComment,
- valid: true
+ valid: true,
};
$form = $('form.js-main-target-form');
$notesContainer = $('ul.main-notes-list');
@@ -163,7 +189,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
mock.restore();
});
- it('updates note and resets edit form', (done) => {
+ it('updates note and resets edit form', done => {
spyOn(this.notes, 'revertNoteEditForm');
spyOn(this.notes, 'setupNewNote');
@@ -175,7 +201,9 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
updatedNote.note = 'bar';
this.notes.updateNote(updatedNote, $targetNote);
- expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
+ expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith(
+ $targetNote,
+ );
expect(this.notes.setupNewNote).toHaveBeenCalled();
done();
@@ -231,17 +259,14 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
note: 'heya',
html: '<div>heya</div>',
};
- $notesList = jasmine.createSpyObj('$notesList', [
- 'find',
- 'append',
- ]);
+ $notesList = jasmine.createSpyObj('$notesList', ['find', 'append']);
notes = jasmine.createSpyObj('notes', [
'setupNewNote',
'refresh',
'collapseLongCommitList',
'updateNotesCount',
- 'putConflictEditWarningInPlace'
+ 'putConflictEditWarningInPlace',
]);
notes.taskList = jasmine.createSpyObj('tasklist', ['init']);
notes.note_ids = [];
@@ -258,7 +283,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
Notes.isNewNote.and.returnValue(true);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(
+ note.html,
+ $notesList,
+ );
});
});
@@ -273,7 +301,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(Notes.animateUpdateNote).toHaveBeenCalledWith(note.html, $note);
+ expect(Notes.animateUpdateNote).toHaveBeenCalledWith(
+ note.html,
+ $note,
+ );
expect(notes.setupNewNote).toHaveBeenCalledWith($newNote);
});
@@ -301,7 +332,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$notesList.find.and.returnValue($note);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
- expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note);
+ expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(
+ note,
+ $note,
+ );
});
});
});
@@ -311,11 +345,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should consider same note text as the same', () => {
const result = Notes.isUpdatedNote(
{
- note: 'initial'
+ note: 'initial',
},
$(`<div>
<div class="original-note-content">initial</div>
- </div>`)
+ </div>`),
);
expect(result).toEqual(false);
@@ -324,11 +358,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should consider same note with trailing newline as the same', () => {
const result = Notes.isUpdatedNote(
{
- note: 'initial\n'
+ note: 'initial\n',
},
$(`<div>
<div class="original-note-content">initial\n</div>
- </div>`)
+ </div>`),
);
expect(result).toEqual(false);
@@ -337,11 +371,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should consider different notes as different', () => {
const result = Notes.isUpdatedNote(
{
- note: 'foo'
+ note: 'foo',
},
$(`<div>
<div class="original-note-content">bar</div>
- </div>`)
+ </div>`),
);
expect(result).toEqual(true);
@@ -397,7 +431,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should call Notes.animateAppendNote', () => {
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.discussion_html, $('.main-notes-list'));
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(
+ note.discussion_html,
+ $('.main-notes-list'),
+ );
});
it('should append to row selected with line_code', () => {
@@ -428,7 +465,10 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('should call Notes.animateAppendNote', () => {
- expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, discussionContainer);
+ expect(Notes.animateAppendNote).toHaveBeenCalledWith(
+ note.html,
+ discussionContainer,
+ );
});
});
});
@@ -461,9 +501,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
beforeEach(() => {
noteHTML = '<div></div>';
- $note = jasmine.createSpyObj('$note', [
- 'replaceWith'
- ]);
+ $note = jasmine.createSpyObj('$note', ['replaceWith']);
$updatedNote = Notes.animateUpdateNote(noteHTML, $note);
});
@@ -501,7 +539,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
<div class="note-text">${sampleComment}</div>
</li>`,
note: sampleComment,
- valid: true
+ valid: true,
};
let $form;
let $notesContainer;
@@ -534,10 +572,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
mockNotesPost();
$('.js-comment-button').click();
- expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true);
+ expect($notesContainer.find('.note.being-posted').length > 0).toEqual(
+ true,
+ );
});
- it('should remove placeholder note when new comment is done posting', (done) => {
+ it('should remove placeholder note when new comment is done posting', done => {
mockNotesPost();
$('.js-comment-button').click();
@@ -549,19 +589,44 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
});
- it('should show actual note element when new comment is done posting', (done) => {
+ describe('postComment', () => {
+ it('disables the submit button', done => {
+ const $submitButton = $form.find('.js-comment-submit-button');
+ expect($submitButton).not.toBeDisabled();
+ const dummyEvent = {
+ preventDefault() {},
+ target: $submitButton,
+ };
+ mock.onPost(NOTES_POST_PATH).replyOnce(() => {
+ expect($submitButton).toBeDisabled();
+ return [200, note];
+ });
+
+ this.notes
+ .postComment(dummyEvent)
+ .then(() => {
+ expect($submitButton).not.toBeDisabled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ it('should show actual note element when new comment is done posting', done => {
mockNotesPost();
$('.js-comment-button').click();
setTimeout(() => {
- expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true);
+ expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(
+ true,
+ );
done();
});
});
- it('should reset Form when new comment is done posting', (done) => {
+ it('should reset Form when new comment is done posting', done => {
mockNotesPost();
$('.js-comment-button').click();
@@ -573,19 +638,24 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
});
- it('should show flash error message when new comment failed to be posted', (done) => {
+ it('should show flash error message when new comment failed to be posted', done => {
mockNotesPostError();
$('.js-comment-button').click();
setTimeout(() => {
- expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true);
+ expect(
+ $notesContainer
+ .parent()
+ .find('.flash-container .flash-text')
+ .is(':visible'),
+ ).toEqual(true);
done();
});
});
- it('should show flash error message when comment failed to be updated', (done) => {
+ it('should show flash error message when comment failed to be updated', done => {
mockNotesPost();
$('.js-comment-button').click();
@@ -606,7 +676,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
.then(() => {
const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
- expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original
+ expect(
+ $updatedNoteEl
+ .find('.note-text')
+ .text()
+ .trim(),
+ ).toEqual(sampleComment); // See if comment reverted back to original
expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown
done();
@@ -620,12 +695,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const note = {
commands_changes: {
assignee_id: 1,
- emoji_award: '100'
+ emoji_award: '100',
},
errors: {
- commands_only: ['Commands applied']
+ commands_only: ['Commands applied'],
},
- valid: false
+ valid: false,
};
let $form;
let $notesContainer;
@@ -640,12 +715,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
window.gon.current_user_fullname = 'Administrator';
gl.awardsHandler = {
addAwardToEmojiBar: () => {},
- scrollToAwards: () => {}
+ scrollToAwards: () => {},
};
gl.GfmAutoComplete = {
dataSources: {
- commands: '/root/test-project/autocomplete_sources/commands'
- }
+ commands: '/root/test-project/autocomplete_sources/commands',
+ },
};
$form = $('form.js-main-target-form');
$notesContainer = $('ul.main-notes-list');
@@ -656,14 +731,18 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
mock.restore();
});
- it('should remove slash command placeholder when comment with slash commands is done posting', (done) => {
+ it('should remove slash command placeholder when comment with slash commands is done posting', done => {
spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
$('.js-comment-button').click();
- expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
+ expect(
+ $notesContainer.find('.system-note.being-posted').length,
+ ).toEqual(1); // Placeholder shown
setTimeout(() => {
- expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
+ expect(
+ $notesContainer.find('.system-note.being-posted').length,
+ ).toEqual(0); // Placeholder removed
done();
});
});
@@ -678,7 +757,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
<div class="note-text">${sampleComment}</div>
</li>`,
note: sampleComment,
- valid: true
+ valid: true,
};
let $form;
let $notesContainer;
@@ -700,7 +779,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
mock.restore();
});
- it('should not render a script tag', (done) => {
+ it('should not render a script tag', done => {
$('.js-comment-button').click();
setTimeout(() => {
@@ -709,8 +788,15 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$noteEl.find('textarea.js-note-text').html(updatedComment);
$noteEl.find('.js-comment-save-button').click();
- const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container');
- expect($updatedNoteEl.find('.note-text').text().trim()).toEqual('');
+ const $updatedNoteEl = $notesContainer
+ .find(`#note_${note.id}`)
+ .find('.js-task-list-container');
+ expect(
+ $updatedNoteEl
+ .find('.note-text')
+ .text()
+ .trim(),
+ ).toEqual('');
done();
});
@@ -730,7 +816,9 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should return form metadata object from form reference', () => {
$form.find('textarea.js-note-text').val(sampleComment);
- const { formData, formContent, formAction } = this.notes.getFormData($form);
+ const { formData, formContent, formAction } = this.notes.getFormData(
+ $form,
+ );
expect(formData.indexOf(sampleComment) > -1).toBe(true);
expect(formContent).toEqual(sampleComment);
@@ -746,7 +834,9 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const { formContent } = this.notes.getFormData($form);
expect(_.escape).toHaveBeenCalledWith(sampleComment);
- expect(formContent).toEqual('&lt;script&gt;alert(&quot;Boom!&quot;);&lt;/script&gt;');
+ expect(formContent).toEqual(
+ '&lt;script&gt;alert(&quot;Boom!&quot;);&lt;/script&gt;',
+ );
});
});
@@ -756,7 +846,8 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('should return true when comment begins with a quick action', () => {
- const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
+ const sampleComment =
+ '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const hasQuickActions = this.notes.hasQuickActions(sampleComment);
expect(hasQuickActions).toBeTruthy();
@@ -780,7 +871,8 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('stripQuickActions', () => {
it('should strip quick actions from the comment which begins with a quick action', () => {
this.notes = new Notes();
- const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
+ const sampleComment =
+ '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('');
@@ -788,7 +880,8 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should strip quick actions from the comment but leaves plain comment if it is present', () => {
this.notes = new Notes();
- const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
+ const sampleComment =
+ '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('Merging this');
@@ -796,7 +889,8 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should NOT strip string that has slashes within', () => {
this.notes = new Notes();
- const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
+ const sampleComment =
+ 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
const stripedComment = this.notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe(sampleComment);
@@ -807,7 +901,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
const availableQuickActions = [
{ name: 'close', description: 'Close this issue', params: [] },
{ name: 'title', description: 'Change title', params: [{}] },
- { name: 'estimate', description: 'Set time estimate', params: [{}] }
+ { name: 'estimate', description: 'Set time estimate', params: [{}] },
];
beforeEach(() => {
@@ -816,17 +910,29 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
it('should return executing quick action description when note has single quick action', () => {
const sampleComment = '/close';
- expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe('Applying command to close this issue');
+ expect(
+ this.notes.getQuickActionDescription(
+ sampleComment,
+ availableQuickActions,
+ ),
+ ).toBe('Applying command to close this issue');
});
it('should return generic multiple quick action description when note has multiple quick actions', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe('Applying multiple commands');
+ expect(
+ this.notes.getQuickActionDescription(
+ sampleComment,
+ availableQuickActions,
+ ),
+ ).toBe('Applying multiple commands');
});
it('should return generic quick action description when available quick actions list is not populated', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(this.notes.getQuickActionDescription(sampleComment)).toBe('Applying command');
+ expect(this.notes.getQuickActionDescription(sampleComment)).toBe(
+ 'Applying command',
+ );
});
});
@@ -856,14 +962,35 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
expect($tempNote.attr('id')).toEqual(uniqueId);
expect($tempNote.hasClass('being-posted')).toBeTruthy();
expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
- $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() {
- expect($(this).attr('href')).toEqual(`/${currentUsername}`);
- });
- expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar);
- expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeFalsy();
- expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual(currentUserFullname);
- expect($tempNoteHeader.find('.note-headline-light').text().trim()).toEqual(`@${currentUsername}`);
- expect($tempNote.find('.note-body .note-text p').text().trim()).toEqual(sampleComment);
+ $tempNote
+ .find('.timeline-icon > a, .note-header-info > a')
+ .each(function() {
+ expect($(this).attr('href')).toEqual(`/${currentUsername}`);
+ });
+ expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(
+ currentUserAvatar,
+ );
+ expect(
+ $tempNote.find('.timeline-content').hasClass('discussion'),
+ ).toBeFalsy();
+ expect(
+ $tempNoteHeader
+ .find('.hidden-xs')
+ .text()
+ .trim(),
+ ).toEqual(currentUserFullname);
+ expect(
+ $tempNoteHeader
+ .find('.note-headline-light')
+ .text()
+ .trim(),
+ ).toEqual(`@${currentUsername}`);
+ expect(
+ $tempNote
+ .find('.note-body .note-text p')
+ .text()
+ .trim(),
+ ).toEqual(sampleComment);
});
it('should return constructed placeholder element for discussion note based on form contents', () => {
@@ -872,11 +999,13 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
uniqueId,
isDiscussionNote: true,
currentUsername,
- currentUserFullname
+ currentUserFullname,
});
expect($tempNote.prop('nodeName')).toEqual('LI');
- expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
+ expect(
+ $tempNote.find('.timeline-content').hasClass('discussion'),
+ ).toBeTruthy();
});
it('should return a escaped user name', () => {
@@ -890,7 +1019,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
currentUserAvatar,
});
const $tempNoteHeader = $tempNote.find('.note-header');
- expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual('Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;');
+ expect(
+ $tempNoteHeader
+ .find('.hidden-xs')
+ .text()
+ .trim(),
+ ).toEqual('Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;');
});
});
@@ -913,7 +1047,12 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
expect($tempNote.attr('id')).toEqual(uniqueId);
expect($tempNote.hasClass('being-posted')).toBeTruthy();
expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
- expect($tempNote.find('.timeline-content i').text().trim()).toEqual(sampleCommandDescription);
+ expect(
+ $tempNote
+ .find('.timeline-content i')
+ .text()
+ .trim(),
+ ).toEqual(sampleCommandDescription);
});
});
@@ -923,7 +1062,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('shows a flash message', () => {
- this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
+ this.notes.addFlash(
+ 'Error message',
+ FLASH_TYPE_ALERT,
+ this.notes.parentTimeline.get(0),
+ );
expect($('.flash-alert').is(':visible')).toBeTruthy();
});
@@ -936,7 +1079,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
it('hides visible flash message', () => {
- this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
+ this.notes.addFlash(
+ 'Error message 1',
+ FLASH_TYPE_ALERT,
+ this.notes.parentTimeline.get(0),
+ );
this.notes.clearFlash();
@@ -944,4 +1091,4 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
});
});
});
-}).call(window);
+}.call(window));
diff --git a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
index ba2e07f02f7..080158a8ee0 100644
--- a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
+++ b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import promoteLabelModal from '~/pages/projects/labels/components/promote_label_modal.vue';
import eventHub from '~/pages/projects/labels/event_hub';
import axios from '~/lib/utils/axios_utils';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Promote label modal', () => {
let vm;
diff --git a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
index bf044fe8fb5..22956929e7b 100644
--- a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
+++ b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
import axios from '~/lib/utils/axios_utils';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Promote milestone modal', () => {
let vm;
diff --git a/spec/javascripts/performance_bar/components/detailed_metric_spec.js b/spec/javascripts/performance_bar/components/detailed_metric_spec.js
new file mode 100644
index 00000000000..c4611dc7662
--- /dev/null
+++ b/spec/javascripts/performance_bar/components/detailed_metric_spec.js
@@ -0,0 +1,80 @@
+import Vue from 'vue';
+import detailedMetric from '~/performance_bar/components/detailed_metric.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('detailedMetric', () => {
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('when the current request has no details', () => {
+ beforeEach(() => {
+ vm = mountComponent(Vue.extend(detailedMetric), {
+ currentRequest: {},
+ metric: 'gitaly',
+ header: 'Gitaly calls',
+ details: 'details',
+ keys: ['feature', 'request'],
+ });
+ });
+
+ it('does not render the element', () => {
+ expect(vm.$el.innerHTML).toEqual(undefined);
+ });
+ });
+
+ describe('when the current request has details', () => {
+ const requestDetails = [
+ { duration: '100', feature: 'find_commit', request: 'abcdef' },
+ { duration: '23', feature: 'rebase_in_progress', request: '' },
+ ];
+
+ beforeEach(() => {
+ vm = mountComponent(Vue.extend(detailedMetric), {
+ currentRequest: {
+ details: {
+ gitaly: {
+ duration: '123ms',
+ calls: '456',
+ details: requestDetails,
+ },
+ },
+ },
+ metric: 'gitaly',
+ header: 'Gitaly calls',
+ details: 'details',
+ keys: ['feature', 'request'],
+ });
+ });
+
+ it('diplays details', () => {
+ expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456');
+ });
+
+ it('adds a modal with a table of the details', () => {
+ vm.$el
+ .querySelectorAll('.performance-bar-modal td strong')
+ .forEach((duration, index) => {
+ expect(duration.innerText).toContain(requestDetails[index].duration);
+ });
+
+ vm.$el
+ .querySelectorAll('.performance-bar-modal td:nth-child(2)')
+ .forEach((feature, index) => {
+ expect(feature.innerText).toContain(requestDetails[index].feature);
+ });
+
+ vm.$el
+ .querySelectorAll('.performance-bar-modal td:nth-child(3)')
+ .forEach((request, index) => {
+ expect(request.innerText).toContain(requestDetails[index].request);
+ });
+ });
+
+ it('displays the metric name', () => {
+ expect(vm.$el.innerText).toContain('gitaly');
+ });
+ });
+});
diff --git a/spec/javascripts/performance_bar/components/performance_bar_app_spec.js b/spec/javascripts/performance_bar/components/performance_bar_app_spec.js
new file mode 100644
index 00000000000..9ab9ab1c9f4
--- /dev/null
+++ b/spec/javascripts/performance_bar/components/performance_bar_app_spec.js
@@ -0,0 +1,88 @@
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import performanceBarApp from '~/performance_bar/components/performance_bar_app.vue';
+import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
+import PerformanceBarStore from '~/performance_bar/stores/performance_bar_store';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import MockAdapter from 'axios-mock-adapter';
+
+describe('performance bar', () => {
+ let mock;
+ let vm;
+
+ beforeEach(() => {
+ const store = new PerformanceBarStore();
+
+ mock = new MockAdapter(axios);
+
+ mock.onGet('/-/peek/results').reply(
+ 200,
+ {
+ data: {
+ gc: {
+ invokes: 0,
+ invoke_time: '0.00',
+ use_size: 0,
+ total_size: 0,
+ total_object: 0,
+ gc_time: '0.00',
+ },
+ host: { hostname: 'web-01' },
+ },
+ },
+ {},
+ );
+
+ vm = mountComponent(Vue.extend(performanceBarApp), {
+ store,
+ env: 'development',
+ requestId: '123',
+ peekUrl: '/-/peek/results',
+ profileUrl: '?lineprofiler=true',
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ mock.restore();
+ });
+
+ it('sets the class to match the environment', () => {
+ expect(vm.$el.getAttribute('class')).toContain('development');
+ });
+
+ describe('loadRequestDetails', () => {
+ beforeEach(() => {
+ spyOn(vm.store, 'addRequest').and.callThrough();
+ });
+
+ it('does nothing if the request cannot be tracked', () => {
+ spyOn(vm.store, 'canTrackRequest').and.callFake(() => false);
+
+ vm.loadRequestDetails('123', 'https://gitlab.com/');
+
+ expect(vm.store.addRequest).not.toHaveBeenCalled();
+ });
+
+ it('adds the request immediately', () => {
+ vm.loadRequestDetails('123', 'https://gitlab.com/');
+
+ expect(vm.store.addRequest).toHaveBeenCalledWith(
+ '123',
+ 'https://gitlab.com/',
+ );
+ });
+
+ it('makes an HTTP request for the request details', () => {
+ spyOn(PerformanceBarService, 'fetchRequestDetails').and.callThrough();
+
+ vm.loadRequestDetails('456', 'https://gitlab.com/');
+
+ expect(PerformanceBarService.fetchRequestDetails).toHaveBeenCalledWith(
+ '/-/peek/results',
+ '456',
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/performance_bar/components/request_selector_spec.js b/spec/javascripts/performance_bar/components/request_selector_spec.js
new file mode 100644
index 00000000000..6108a29f8c4
--- /dev/null
+++ b/spec/javascripts/performance_bar/components/request_selector_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import requestSelector from '~/performance_bar/components/request_selector.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('request selector', () => {
+ const requests = [
+ { id: '123', url: 'https://gitlab.com/' },
+ {
+ id: '456',
+ url: 'https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1',
+ },
+ {
+ id: '789',
+ url:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1.json?serializer=widget',
+ },
+ ];
+
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Vue.extend(requestSelector), {
+ requests,
+ currentRequest: requests[1],
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ function optionText(requestId) {
+ return vm.$el.querySelector(`[value='${requestId}']`).innerText.trim();
+ }
+
+ it('displays the last component of the path', () => {
+ expect(optionText(requests[2].id)).toEqual('1.json?serializer=widget');
+ });
+
+ it('keeps the last two components of the path when the last component is numeric', () => {
+ expect(optionText(requests[1].id)).toEqual('merge_requests/1');
+ });
+
+ it('ignores trailing slashes', () => {
+ expect(optionText(requests[0].id)).toEqual('gitlab.com');
+ });
+});
diff --git a/spec/javascripts/performance_bar/components/simple_metric_spec.js b/spec/javascripts/performance_bar/components/simple_metric_spec.js
new file mode 100644
index 00000000000..98b843e9711
--- /dev/null
+++ b/spec/javascripts/performance_bar/components/simple_metric_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import simpleMetric from '~/performance_bar/components/simple_metric.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('simpleMetric', () => {
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('when the current request has no details', () => {
+ beforeEach(() => {
+ vm = mountComponent(Vue.extend(simpleMetric), {
+ currentRequest: {},
+ metric: 'gitaly',
+ });
+ });
+
+ it('does not display details', () => {
+ expect(vm.$el.innerText).not.toContain('/');
+ });
+
+ it('displays the metric name', () => {
+ expect(vm.$el.innerText).toContain('gitaly');
+ });
+ });
+
+ describe('when the current request has details', () => {
+ beforeEach(() => {
+ vm = mountComponent(Vue.extend(simpleMetric), {
+ currentRequest: {
+ details: { gitaly: { duration: '123ms', calls: '456' } },
+ },
+ metric: 'gitaly',
+ });
+ });
+
+ it('diplays details', () => {
+ expect(vm.$el.innerText.replace(/\s+/g, ' ')).toContain('123ms / 456');
+ });
+
+ it('displays the metric name', () => {
+ expect(vm.$el.innerText).toContain('gitaly');
+ });
+ });
+});
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index faaf710cf6f..b0d714cbefb 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import initCopyAsGFM from '~/behaviors/copy_as_gfm';
+import initCopyAsGFM from '~/behaviors/markdown/copy_as_gfm';
import ShortcutsIssuable from '~/shortcuts_issuable';
initCopyAsGFM();
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index 31710551399..d9c03296857 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -23,9 +23,7 @@ const metricsMockData = {
memory_values: [
{
metric: {},
- values: [
- [1493716685, '4.30859375'],
- ],
+ values: [[1493716685, '4.30859375']],
},
],
},
@@ -53,7 +51,8 @@ const createComponent = () => {
const messages = {
loadingMetrics: 'Loading deployment statistics',
- hasMetrics: 'Memory usage unchanged from 0MB to 0MB',
+ hasMetrics:
+ '<a href="/root/acets-review-apps/environments/15/metrics"> Memory </a> usage is <b> unchanged </b> at 0MB',
loadFailed: 'Failed to load deployment statistics',
metricsUnavailable: 'Deployment statistics are not available currently',
};
@@ -92,26 +91,26 @@ describe('MemoryUsage', () => {
});
describe('computed', () => {
- describe('memoryChangeType', () => {
- it('should return "increased" if memoryFrom value is less than memoryTo value', () => {
+ describe('memoryChangeMessage', () => {
+ it('should contain "increased" if memoryFrom value is less than memoryTo value', () => {
vm.memoryFrom = 4.28;
vm.memoryTo = 9.13;
- expect(vm.memoryChangeType).toEqual('increased');
+ expect(vm.memoryChangeMessage.indexOf('increased')).not.toEqual('-1');
});
- it('should return "decreased" if memoryFrom value is less than memoryTo value', () => {
+ it('should contain "decreased" if memoryFrom value is less than memoryTo value', () => {
vm.memoryFrom = 9.13;
vm.memoryTo = 4.28;
- expect(vm.memoryChangeType).toEqual('decreased');
+ expect(vm.memoryChangeMessage.indexOf('decreased')).not.toEqual('-1');
});
- it('should return "unchanged" if memoryFrom value equal to memoryTo value', () => {
+ it('should contain "unchanged" if memoryFrom value equal to memoryTo value', () => {
vm.memoryFrom = 1;
vm.memoryTo = 1;
- expect(vm.memoryChangeType).toEqual('unchanged');
+ expect(vm.memoryChangeMessage.indexOf('unchanged')).not.toEqual('-1');
});
});
});
@@ -130,7 +129,13 @@ describe('MemoryUsage', () => {
describe('computeGraphData', () => {
it('should populate sparkline graph', () => {
vm.computeGraphData(metrics, deployment_time);
- const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm;
+ const {
+ hasMetrics,
+ memoryMetrics,
+ deploymentTime,
+ memoryFrom,
+ memoryTo,
+ } = vm;
expect(hasMetrics).toBeTruthy();
expect(memoryMetrics.length > 0).toBeTruthy();
@@ -141,20 +146,26 @@ describe('MemoryUsage', () => {
});
describe('loadMetrics', () => {
- const returnServicePromise = () => new Promise((resolve) => {
- resolve({
- data: metricsMockData,
+ const returnServicePromise = () =>
+ new Promise(resolve => {
+ resolve({
+ data: metricsMockData,
+ });
});
- });
- it('should load metrics data using MRWidgetService', (done) => {
- spyOn(MRWidgetService, 'fetchMetrics').and.returnValue(returnServicePromise(true));
+ it('should load metrics data using MRWidgetService', done => {
+ spyOn(MRWidgetService, 'fetchMetrics').and.returnValue(
+ returnServicePromise(true),
+ );
spyOn(vm, 'computeGraphData');
vm.loadMetrics();
setTimeout(() => {
expect(MRWidgetService.fetchMetrics).toHaveBeenCalledWith(url);
- expect(vm.computeGraphData).toHaveBeenCalledWith(metrics, deployment_time);
+ expect(vm.computeGraphData).toHaveBeenCalledWith(
+ metrics,
+ deployment_time,
+ );
done();
}, 333);
});
@@ -167,51 +178,67 @@ describe('MemoryUsage', () => {
expect(el.querySelector('.js-usage-info')).toBeDefined();
});
- it('should show loading metrics message while metrics are being loaded', (done) => {
+ it('should show loading metrics message while metrics are being loaded', done => {
vm.loadingMetrics = true;
vm.hasMetrics = false;
vm.loadFailed = false;
Vue.nextTick(() => {
- expect(el.querySelector('.js-usage-info.usage-info-loading')).toBeDefined();
- expect(el.querySelector('.js-usage-info .usage-info-load-spinner')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadingMetrics);
+ expect(
+ el.querySelector('.js-usage-info.usage-info-loading'),
+ ).toBeDefined();
+ expect(
+ el.querySelector('.js-usage-info .usage-info-load-spinner'),
+ ).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(
+ messages.loadingMetrics,
+ );
done();
});
});
- it('should show deployment memory usage when metrics are loaded', (done) => {
+ it('should show deployment memory usage when metrics are loaded', done => {
vm.loadingMetrics = false;
vm.hasMetrics = true;
vm.loadFailed = false;
Vue.nextTick(() => {
expect(el.querySelector('.memory-graph-container')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.hasMetrics);
+ expect(el.querySelector('.js-usage-info').innerText).toContain(
+ messages.hasMetrics,
+ );
done();
});
});
- it('should show failure message when metrics loading failed', (done) => {
+ it('should show failure message when metrics loading failed', done => {
vm.loadingMetrics = false;
vm.hasMetrics = false;
vm.loadFailed = true;
Vue.nextTick(() => {
- expect(el.querySelector('.js-usage-info.usage-info-failed')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.loadFailed);
+ expect(
+ el.querySelector('.js-usage-info.usage-info-failed'),
+ ).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(
+ messages.loadFailed,
+ );
done();
});
});
- it('should show metrics unavailable message when metrics loading failed', (done) => {
+ it('should show metrics unavailable message when metrics loading failed', done => {
vm.loadingMetrics = false;
vm.hasMetrics = false;
vm.loadFailed = false;
Vue.nextTick(() => {
- expect(el.querySelector('.js-usage-info.usage-info-unavailable')).toBeDefined();
- expect(el.querySelector('.js-usage-info').innerText).toContain(messages.metricsUnavailable);
+ expect(
+ el.querySelector('.js-usage-info.usage-info-unavailable'),
+ ).toBeDefined();
+ expect(el.querySelector('.js-usage-info').innerText).toContain(
+ messages.metricsUnavailable,
+ );
done();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index 5323523abc0..fcbd8169bc7 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import removeBreakLine from 'spec/helpers/vue_component_helper';
describe('MRWidgetConflicts', () => {
let Component;
@@ -78,8 +79,9 @@ describe('MRWidgetConflicts', () => {
});
it('should tell you to rebase locally', () => {
- expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('Fast-forward merge is not possible.');
- expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('To merge this request, first rebase locally');
+ expect(
+ removeBreakLine(vm.$el.textContent).trim(),
+ ).toContain('Fast-forward merge is not possible. To merge this request, first rebase locally.');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
index baacbc03fb1..894dbe3382f 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import removeBreakLine from 'spec/helpers/vue_component_helper';
describe('MRWidgetPipelineBlocked', () => {
let vm;
@@ -18,6 +19,8 @@ describe('MRWidgetPipelineBlocked', () => {
});
it('renders information text', () => {
- expect(vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ')).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
+ expect(
+ removeBreakLine(vm.$el.textContent).trim(),
+ ).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
index 4c67504b642..b02af94d03a 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -1,16 +1,25 @@
import Vue from 'vue';
-import shaMismatchComponent from '~/vue_merge_request_widget/components/states/mr_widget_sha_mismatch';
+import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import removeBreakLine from 'spec/helpers/vue_component_helper';
-describe('MRWidgetSHAMismatch', () => {
- describe('template', () => {
- const Component = Vue.extend(shaMismatchComponent);
- const vm = new Component({
- el: document.createElement('div'),
- });
- it('should have correct elements', () => {
- expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging');
- });
+describe('ShaMismatch', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ShaMismatch);
+ vm = mountComponent(Component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render information message', () => {
+ expect(vm.$el.querySelector('button').disabled).toEqual(true);
+
+ expect(
+ removeBreakLine(vm.$el.textContent).trim(),
+ ).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging');
});
});
diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/deprecated_modal_spec.js
index d01a94c25e5..59d4e549a91 100644
--- a/spec/javascripts/vue_shared/components/modal_spec.js
+++ b/spec/javascripts/vue_shared/components/deprecated_modal_spec.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import Vue from 'vue';
-import modal from '~/vue_shared/components/modal.vue';
+import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-const modalComponent = Vue.extend(modal);
+const modalComponent = Vue.extend(DeprecatedModal);
-describe('Modal', () => {
+describe('DeprecatedModal', () => {
let vm;
afterEach(() => {
diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
index 818ef0af3c2..3e708f865c8 100644
--- a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('toolbar', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
index 8daaf018396..6fe95153204 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -3,9 +3,9 @@ import Vue from 'vue';
import LabelsSelect from '~/labels_select';
import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
-import { mockConfig, mockLabels } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockConfig, mockLabels } from './mock_data';
const createComponent = (config = mockConfig) => {
const Component = Vue.extend(baseComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
index ec63ac306d0..f25c70db125 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
-import { mockConfig, mockLabels } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockConfig, mockLabels } from './mock_data';
const componentConfig = Object.assign({}, mockConfig, {
fieldName: 'label_id[]',
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index 5cb4bb6fea6..ce559fe0335 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
-import { mockSuggestedColors } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockSuggestedColors } from './mock_data';
const createComponent = (headerTitle) => {
const Component = Vue.extend(dropdownCreateLabelComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
index 0f4fa716f8a..debeab25bd6 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
-import { mockConfig } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockConfig } from './mock_data';
const createComponent = (
labelsWebUrl = mockConfig.labelsWebUrl,
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
index 325fa47c957..cdf234bb0c4 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(dropdownHeaderComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
index 703b87498c7..88733922a59 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownHiddenInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue';
-import { mockLabels } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockLabels } from './mock_data';
const createComponent = (name = 'label_id[]', label = mockLabels[0]) => {
const Component = Vue.extend(dropdownHiddenInputComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
index 69e11d966c2..57608d957e7 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(dropdownSearchInputComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
index c3580933072..7c3d2711f65 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = (canEdit = true) => {
const Component = Vue.extend(dropdownTitleComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 93b42795bea..39040670a87 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
-import { mockLabels } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockLabels } from './mock_data';
const createComponent = (labels = mockLabels) => {
const Component = Vue.extend(dropdownValueCollapsedComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 66e0957b431..4397b00acfa 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -2,9 +2,9 @@ import Vue from 'vue';
import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
-import { mockConfig, mockLabels } from './mock_data';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import { mockConfig, mockLabels } from './mock_data';
const createComponent = (
labels = mockLabels,
diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index 5100f5737c2..84688845fa5 100644
--- a/spec/lib/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
@@ -278,6 +278,10 @@ describe Backup::Manager do
connection.directories.create(key: Gitlab.config.backup.upload.remote_directory)
end
+ after do
+ Fog.unmock!
+ end
+
context 'target path' do
it 'uses the tar filename by default' do
expect_any_instance_of(Fog::Collection).to receive(:create)
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index a9b5ed1112a..03573c304aa 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -33,7 +33,7 @@ describe Backup::Repository do
let(:timestamp) { Time.utc(2017, 3, 22) }
let(:temp_dirs) do
Gitlab.config.repositories.storages.map do |name, storage|
- File.join(storage['path'], '..', 'repositories.old.' + timestamp.to_i.to_s)
+ File.join(storage.legacy_disk_path, '..', 'repositories.old.' + timestamp.to_i.to_s)
end
end
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index b502daea418..a50329473ad 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -122,14 +122,10 @@ describe Banzai::Filter::AutolinkFilter do
end
it 'does not include trailing punctuation' do
- doc = filter("See #{link}.")
- expect(doc.at_css('a').text).to eq link
-
- doc = filter("See #{link}, ok?")
- expect(doc.at_css('a').text).to eq link
-
- doc = filter("See #{link}...")
- expect(doc.at_css('a').text).to eq link
+ ['.', ', ok?', '...', '?', '!', ': is that ok?'].each do |trailing_punctuation|
+ doc = filter("See #{link}#{trailing_punctuation}")
+ expect(doc.at_css('a').text).to eq link
+ end
end
it 'includes trailing punctuation when part of a balanced pair' do
@@ -171,6 +167,15 @@ describe Banzai::Filter::AutolinkFilter do
expect(actual).to eq(expected_complicated_link)
end
+ it 'does not double-encode HTML entities' do
+ encoded_link = "#{link}?foo=bar&amp;baz=quux"
+ expected_encoded_link = %Q{<a href="#{encoded_link}">#{encoded_link}</a>}
+ actual = unescape(filter(encoded_link).to_html)
+
+ expect(actual).to eq(Rinku.auto_link(encoded_link))
+ expect(actual).to eq(expected_encoded_link)
+ end
+
it 'does not include trailing HTML entities' do
doc = filter("See &lt;&lt;&lt;#{link}&gt;&gt;&gt;")
diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
index 17347768a49..a5373517ac8 100644
--- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
@@ -8,6 +8,7 @@ describe Banzai::Filter::IssuableStateFilter do
let(:context) { { current_user: user, issuable_state_filter_enabled: true } }
let(:closed_issue) { create_issue(:closed) }
let(:project) { create(:project, :public) }
+ let(:group) { create(:group) }
let(:other_project) { create(:project, :public) }
def create_link(text, data)
@@ -77,6 +78,13 @@ describe Banzai::Filter::IssuableStateFilter do
expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)} (closed)")
end
+ it 'handles references from group scopes' do
+ link = create_link(closed_issue.to_reference(other_project), issue: closed_issue.id, reference_type: 'issue')
+ doc = filter(link, context.merge(project: nil, group: group))
+
+ expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)} (closed)")
+ end
+
it 'skips cross project references if the user cannot read cross project' do
expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
link = create_link(closed_issue.to_reference(other_project), issue: closed_issue.id, reference_type: 'issue')
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 2a0e19ae796..e1782cff81a 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -48,7 +48,7 @@ module Gitlab
},
'images' => {
input: 'image:https://localhost.com/image.png[Alt text" onerror="alert(7)]',
- output: "<img src=\"https://localhost.com/image.png\" alt=\"Alt text\">"
+ output: "<div>\n<p><span><img src=\"https://localhost.com/image.png\" alt='Alt text\" onerror=\"alert(7)'></span></p>\n</div>"
},
'pre' => {
input: '```mypre"><script>alert(3)</script>',
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index f969f9e8e38..18cef8ec996 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -315,13 +315,19 @@ describe Gitlab::Auth do
it "tries to autheticate with db before ldap" do
expect(Gitlab::Auth::LDAP::Authentication).not_to receive(:login)
- gl_auth.find_with_user_password(username, password)
+ expect(gl_auth.find_with_user_password(username, password)).to eq(user)
+ end
+
+ it "does not find user by using ldap as fallback to for authentication" do
+ expect(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(nil)
+
+ expect(gl_auth.find_with_user_password('ldap_user', 'password')).to be_nil
end
- it "uses ldap as fallback to for authentication" do
- expect(Gitlab::Auth::LDAP::Authentication).to receive(:login)
+ it "find new user by using ldap as fallback to for authentication" do
+ expect(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(user)
- gl_auth.find_with_user_password('ldap_user', 'password')
+ expect(gl_auth.find_with_user_password('ldap_user', 'password')).to eq(user)
end
end
diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
index 5cb1f4deb5f..0dc3705825d 100644
--- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
@@ -54,7 +54,7 @@ describe ::Gitlab::BareRepositoryImport::Repository do
context 'hashed storage' do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:repository_storage) { 'default' }
- let(:root_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:root_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' }
let(:hashed_path) { "@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" }
let(:repo_path) { File.join(root_path, "#{hashed_path}.git") }
diff --git a/spec/lib/gitlab/checks/project_moved_spec.rb b/spec/lib/gitlab/checks/project_moved_spec.rb
index e263d29656c..8e9386b1ba1 100644
--- a/spec/lib/gitlab/checks/project_moved_spec.rb
+++ b/spec/lib/gitlab/checks/project_moved_spec.rb
@@ -44,44 +44,17 @@ describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do
end
describe '#message' do
- context 'when the push is rejected' do
- it 'returns a redirect message telling the user to try again' do
- project_moved = described_class.new(project, user, 'http', 'foo/bar')
- message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
- "\n\nPlease update your Git remote:" +
- "\n\n git remote set-url origin #{project.http_url_to_repo} and try again.\n"
+ it 'returns a redirect message' do
+ project_moved = described_class.new(project, user, 'http', 'foo/bar')
+ message = <<~MSG
+ Project 'foo/bar' was moved to '#{project.full_path}'.
- expect(project_moved.message(rejected: true)).to eq(message)
- end
- end
+ Please update your Git remote:
- context 'when the push is not rejected' do
- it 'returns a redirect message' do
- project_moved = described_class.new(project, user, 'http', 'foo/bar')
- message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
- "\n\nPlease update your Git remote:" +
- "\n\n git remote set-url origin #{project.http_url_to_repo}\n"
+ git remote set-url origin #{project.http_url_to_repo}
+ MSG
- expect(project_moved.message).to eq(message)
- end
- end
- end
-
- describe '#permanent_redirect?' do
- context 'with a permanent RedirectRoute' do
- it 'returns true' do
- project.route.create_redirect('foo/bar', permanent: true)
- project_moved = described_class.new(project, user, 'http', 'foo/bar')
- expect(project_moved.permanent_redirect?).to be_truthy
- end
- end
-
- context 'without a permanent RedirectRoute' do
- it 'returns false' do
- project.route.create_redirect('foo/bar')
- project_moved = described_class.new(project, user, 'http', 'foo/bar')
- expect(project_moved.permanent_redirect?).to be_falsy
- end
+ expect(project_moved.message).to eq(message)
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
index 1b03227d67b..dc12ba076bc 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
@@ -5,23 +5,23 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
set(:user) { create(:user) }
let(:pipeline) do
- build(:ci_pipeline_with_one_job, project: project,
- ref: 'master')
+ build(:ci_empty_pipeline, project: project, ref: 'master')
end
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
- project: project,
- current_user: user, seeds_block: nil)
+ project: project, current_user: user)
end
let(:step) { described_class.new(pipeline, command) }
- before do
- step.perform!
- end
-
context 'when pipeline is ready to be saved' do
+ before do
+ pipeline.stages.build(name: 'test', project: project)
+
+ step.perform!
+ end
+
it 'saves a pipeline' do
expect(pipeline).to be_persisted
end
@@ -32,6 +32,7 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
it 'creates stages' do
expect(pipeline.reload.stages).to be_one
+ expect(pipeline.stages.first).to be_persisted
end
end
@@ -40,6 +41,10 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
build(:ci_pipeline, project: project, ref: nil)
end
+ before do
+ step.perform!
+ end
+
it 'breaks the chain' do
expect(step.break?).to be true
end
@@ -49,18 +54,4 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
.to include /Failed to persist the pipeline/
end
end
-
- context 'when there is a seed block present' do
- let(:seeds) { spy('pipeline seeds') }
-
- let(:command) do
- double('command', project: project,
- current_user: user,
- seeds_block: seeds)
- end
-
- it 'executes the block' do
- expect(seeds).to have_received(:call).with(pipeline)
- end
- end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
new file mode 100644
index 00000000000..2258ae83f38
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -0,0 +1,153 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Populate do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ let(:pipeline) do
+ build(:ci_pipeline_with_one_job, project: project,
+ ref: 'master')
+ end
+
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ seeds_block: nil)
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ context 'when pipeline doesn not have seeds block' do
+ before do
+ step.perform!
+ end
+
+ it 'does not persist the pipeline' do
+ expect(pipeline).not_to be_persisted
+ end
+
+ it 'does not break the chain' do
+ expect(step.break?).to be false
+ end
+
+ it 'populates pipeline with stages' do
+ expect(pipeline.stages).to be_one
+ expect(pipeline.stages.first).not_to be_persisted
+ end
+
+ it 'populates pipeline with builds' do
+ expect(pipeline.builds).to be_one
+ expect(pipeline.builds.first).not_to be_persisted
+ expect(pipeline.stages.first.builds).to be_one
+ expect(pipeline.stages.first.builds.first).not_to be_persisted
+ end
+ end
+
+ context 'when pipeline is empty' do
+ let(:config) do
+ { rspec: {
+ script: 'ls',
+ only: ['something']
+ } }
+ end
+
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, config: config)
+ end
+
+ before do
+ step.perform!
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+
+ it 'appends an error about missing stages' do
+ expect(pipeline.errors.to_a)
+ .to include 'No stages / jobs for this pipeline.'
+ end
+ end
+
+ context 'when pipeline has validation errors' do
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, ref: nil)
+ end
+
+ before do
+ step.perform!
+ end
+
+ it 'breaks the chain' do
+ expect(step.break?).to be true
+ end
+
+ it 'appends validation error' do
+ expect(pipeline.errors.to_a)
+ .to include 'Failed to build the pipeline!'
+ end
+ end
+
+ context 'when there is a seed blocks present' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ seeds_block: seeds_block)
+ end
+
+ context 'when seeds block builds some resources' do
+ let(:seeds_block) do
+ ->(pipeline) { pipeline.variables.build(key: 'VAR', value: '123') }
+ end
+
+ it 'populates pipeline with resources described in the seeds block' do
+ step.perform!
+
+ expect(pipeline).not_to be_persisted
+ expect(pipeline.variables).not_to be_empty
+ expect(pipeline.variables.first).not_to be_persisted
+ expect(pipeline.variables.first.key).to eq 'VAR'
+ expect(pipeline.variables.first.value).to eq '123'
+ end
+ end
+
+ context 'when seeds block tries to persist some resources' do
+ let(:seeds_block) do
+ ->(pipeline) { pipeline.variables.create!(key: 'VAR', value: '123') }
+ end
+
+ it 'raises exception' do
+ expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved)
+ end
+ end
+ end
+
+ context 'when pipeline gets persisted during the process' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ it 'raises error' do
+ expect { step.perform! }.to raise_error(described_class::PopulateError)
+ end
+ end
+
+ context 'when using only/except build policies' do
+ let(:config) do
+ { rspec: { script: 'rspec', stage: 'test', only: ['master'] },
+ prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] } }
+ end
+
+ let(:pipeline) do
+ build(:ci_pipeline, ref: 'master', config: config)
+ end
+
+ it 'populates pipeline according to used policies' do
+ step.perform!
+
+ expect(pipeline.stages.size).to eq 1
+ expect(pipeline.builds.size).to eq 1
+ expect(pipeline.builds.first.name).to eq 'rspec'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
index 5c12c6e6392..c53294d091c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
@@ -76,28 +76,6 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
end
end
- context 'when pipeline has no stages / jobs' do
- let(:config) do
- { rspec: {
- script: 'ls',
- only: ['something']
- } }
- end
-
- let(:pipeline) do
- build(:ci_pipeline, project: project, config: config)
- end
-
- it 'appends an error about missing stages' do
- expect(pipeline.errors.to_a)
- .to include 'No stages / jobs for this pipeline.'
- end
-
- it 'breaks the chain' do
- expect(step.break?).to be true
- end
- end
-
context 'when pipeline contains configuration validation errors' do
let(:config) { { rspec: {} } }
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
new file mode 100644
index 00000000000..116573379e0
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -0,0 +1,242 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Seed::Build do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ let(:attributes) do
+ { name: 'rspec',
+ ref: 'master',
+ commands: 'rspec' }
+ end
+
+ subject do
+ described_class.new(pipeline, attributes)
+ end
+
+ describe '#attributes' do
+ it 'returns hash attributes of a build' do
+ expect(subject.attributes).to be_a Hash
+ expect(subject.attributes)
+ .to include(:name, :project, :ref, :commands)
+ end
+ end
+
+ describe '#user=' do
+ let(:user) { build(:user) }
+
+ it 'assignes user to a build' do
+ subject.user = user
+
+ expect(subject.attributes).to include(user: user)
+ end
+ end
+
+ describe '#to_resource' do
+ it 'returns a valid build resource' do
+ expect(subject.to_resource).to be_a(::Ci::Build)
+ expect(subject.to_resource).to be_valid
+ end
+
+ it 'memoizes a resource object' do
+ build = subject.to_resource
+
+ expect(build.object_id).to eq subject.to_resource.object_id
+ end
+
+ it 'can not be persisted without explicit assignment' do
+ build = subject.to_resource
+
+ pipeline.save!
+
+ expect(build).not_to be_persisted
+ end
+ end
+
+ describe 'applying only/except policies' do
+ context 'when no branch policy is specified' do
+ let(:attributes) { { name: 'rspec' } }
+
+ it { is_expected.to be_included }
+ end
+
+ context 'when branch policy does not match' do
+ context 'when using only' do
+ let(:attributes) { { name: 'rspec', only: { refs: ['deploy'] } } }
+
+ it { is_expected.not_to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) { { name: 'rspec', except: { refs: ['deploy'] } } }
+
+ it { is_expected.to be_included }
+ end
+ end
+
+ context 'when branch regexp policy does not match' do
+ context 'when using only' do
+ let(:attributes) { { name: 'rspec', only: { refs: ['/^deploy$/'] } } }
+
+ it { is_expected.not_to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) { { name: 'rspec', except: { refs: ['/^deploy$/'] } } }
+
+ it { is_expected.to be_included }
+ end
+ end
+
+ context 'when branch policy matches' do
+ context 'when using only' do
+ let(:attributes) { { name: 'rspec', only: { refs: %w[deploy master] } } }
+
+ it { is_expected.to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) { { name: 'rspec', except: { refs: %w[deploy master] } } }
+
+ it { is_expected.not_to be_included }
+ end
+ end
+
+ context 'when keyword policy matches' do
+ context 'when using only' do
+ let(:attributes) { { name: 'rspec', only: { refs: ['branches'] } } }
+
+ it { is_expected.to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) { { name: 'rspec', except: { refs: ['branches'] } } }
+
+ it { is_expected.not_to be_included }
+ end
+ end
+
+ context 'when keyword policy does not match' do
+ context 'when using only' do
+ let(:attributes) { { name: 'rspec', only: { refs: ['tags'] } } }
+
+ it { is_expected.not_to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) { { name: 'rspec', except: { refs: ['tags'] } } }
+
+ it { is_expected.to be_included }
+ end
+ end
+
+ context 'when keywords and pipeline source policy matches' do
+ possibilities = [%w[pushes push],
+ %w[web web],
+ %w[triggers trigger],
+ %w[schedules schedule],
+ %w[api api],
+ %w[external external]]
+
+ context 'when using only' do
+ possibilities.each do |keyword, source|
+ context "when using keyword `#{keyword}` and source `#{source}`" do
+ let(:pipeline) do
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
+ end
+
+ let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } }
+
+ it { is_expected.to be_included }
+ end
+ end
+ end
+
+ context 'when using except' do
+ possibilities.each do |keyword, source|
+ context "when using keyword `#{keyword}` and source `#{source}`" do
+ let(:pipeline) do
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
+ end
+
+ let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } }
+
+ it { is_expected.not_to be_included }
+ end
+ end
+ end
+ end
+
+ context 'when keywords and pipeline source does not match' do
+ possibilities = [%w[pushes web],
+ %w[web push],
+ %w[triggers schedule],
+ %w[schedules external],
+ %w[api trigger],
+ %w[external api]]
+
+ context 'when using only' do
+ possibilities.each do |keyword, source|
+ context "when using keyword `#{keyword}` and source `#{source}`" do
+ let(:pipeline) do
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
+ end
+
+ let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } }
+
+ it { is_expected.not_to be_included }
+ end
+ end
+ end
+
+ context 'when using except' do
+ possibilities.each do |keyword, source|
+ context "when using keyword `#{keyword}` and source `#{source}`" do
+ let(:pipeline) do
+ build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
+ end
+
+ let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } }
+
+ it { is_expected.to be_included }
+ end
+ end
+ end
+ end
+
+ context 'when repository path matches' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } }
+ end
+
+ it { is_expected.to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } }
+ end
+
+ it { is_expected.not_to be_included }
+ end
+ end
+
+ context 'when repository path does not matches' do
+ context 'when using only' do
+ let(:attributes) do
+ { name: 'rspec', only: { refs: ['branches@fork'] } }
+ end
+
+ it { is_expected.not_to be_included }
+ end
+
+ context 'when using except' do
+ let(:attributes) do
+ { name: 'rspec', except: { refs: ['branches@fork'] } }
+ end
+
+ it { is_expected.to be_included }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
new file mode 100644
index 00000000000..8f0bf40d624
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Seed::Stage do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
+ let(:attributes) do
+ { name: 'test',
+ index: 0,
+ builds: [{ name: 'rspec' },
+ { name: 'spinach' },
+ { name: 'deploy', only: { refs: ['feature'] } }] }
+ end
+
+ subject do
+ described_class.new(pipeline, attributes)
+ end
+
+ describe '#size' do
+ it 'returns a number of jobs in the stage' do
+ expect(subject.size).to eq 2
+ end
+ end
+
+ describe '#attributes' do
+ it 'returns hash attributes of a stage' do
+ expect(subject.attributes).to be_a Hash
+ expect(subject.attributes).to include(:name, :project)
+ end
+ end
+
+ describe '#included?' do
+ context 'when it contains builds seeds' do
+ let(:attributes) do
+ { name: 'test',
+ index: 0,
+ builds: [{ name: 'deploy', only: { refs: ['master'] } }] }
+ end
+
+ it { is_expected.to be_included }
+ end
+
+ context 'when it does not contain build seeds' do
+ let(:attributes) do
+ { name: 'test',
+ index: 0,
+ builds: [{ name: 'deploy', only: { refs: ['feature'] } }] }
+ end
+
+ it { is_expected.not_to be_included }
+ end
+ end
+
+ describe '#seeds' do
+ it 'returns build seeds' do
+ expect(subject.seeds).to all(be_a Gitlab::Ci::Pipeline::Seed::Build)
+ end
+
+ it 'returns build seeds including valid attributes' do
+ expect(subject.seeds.size).to eq 2
+ expect(subject.seeds.map(&:attributes)).to all(include(ref: 'master'))
+ expect(subject.seeds.map(&:attributes)).to all(include(tag: false))
+ expect(subject.seeds.map(&:attributes)).to all(include(project: pipeline.project))
+ expect(subject.seeds.map(&:attributes))
+ .to all(include(trigger_request: pipeline.trigger_requests.first))
+ end
+
+ context 'when a ref is protected' do
+ before do
+ allow_any_instance_of(Project).to receive(:protected_for?).and_return(true)
+ end
+
+ it 'returns protected builds' do
+ expect(subject.seeds.map(&:attributes)).to all(include(protected: true))
+ end
+ end
+
+ context 'when a ref is not protected' do
+ before do
+ allow_any_instance_of(Project).to receive(:protected_for?).and_return(false)
+ end
+
+ it 'returns unprotected builds' do
+ expect(subject.seeds.map(&:attributes)).to all(include(protected: false))
+ end
+ end
+
+ it 'filters seeds using only/except policies' do
+ expect(subject.seeds.map(&:attributes)).to satisfy do |seeds|
+ seeds.any? { |hash| hash.fetch(:name) == 'rspec' }
+ end
+
+ expect(subject.seeds.map(&:attributes)).not_to satisfy do |seeds|
+ seeds.any? { |hash| hash.fetch(:name) == 'deploy' }
+ end
+ end
+ end
+
+ describe '#user=' do
+ let(:user) { build(:user) }
+
+ it 'assignes relevant pipeline attributes' do
+ subject.user = user
+
+ expect(subject.seeds.map(&:attributes)).to all(include(user: user))
+ end
+ end
+
+ describe '#to_resource' do
+ it 'builds a valid stage object with all builds' do
+ subject.to_resource.save!
+
+ expect(pipeline.reload.stages.count).to eq 1
+ expect(pipeline.reload.builds.count).to eq 2
+ expect(pipeline.builds).to all(satisfy { |job| job.stage_id.present? })
+ expect(pipeline.builds).to all(satisfy { |job| job.pipeline.present? })
+ expect(pipeline.builds).to all(satisfy { |job| job.project.present? })
+ expect(pipeline.stages)
+ .to all(satisfy { |stage| stage.pipeline.present? })
+ expect(pipeline.stages)
+ .to all(satisfy { |stage| stage.project.present? })
+ end
+
+ it 'can not be persisted without explicit pipeline assignment' do
+ stage = subject.to_resource
+
+ pipeline.save!
+
+ expect(stage).not_to be_persisted
+ expect(pipeline.reload.stages.count).to eq 0
+ expect(pipeline.reload.builds.count).to eq 0
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb
deleted file mode 100644
index 3fe8d50c49a..00000000000
--- a/spec/lib/gitlab/ci/stage/seed_spec.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Stage::Seed do
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- let(:builds) do
- [{ name: 'rspec' }, { name: 'spinach' }]
- end
-
- subject do
- described_class.new(pipeline, 'test', builds)
- end
-
- describe '#size' do
- it 'returns a number of jobs in the stage' do
- expect(subject.size).to eq 2
- end
- end
-
- describe '#stage' do
- it 'returns hash attributes of a stage' do
- expect(subject.stage).to be_a Hash
- expect(subject.stage).to include(:name, :project)
- end
- end
-
- describe '#builds' do
- it 'returns hash attributes of all builds' do
- expect(subject.builds.size).to eq 2
- expect(subject.builds).to all(include(ref: 'master'))
- expect(subject.builds).to all(include(tag: false))
- expect(subject.builds).to all(include(project: pipeline.project))
- expect(subject.builds)
- .to all(include(trigger_request: pipeline.trigger_requests.first))
- end
-
- context 'when a ref is protected' do
- before do
- allow_any_instance_of(Project).to receive(:protected_for?).and_return(true)
- end
-
- it 'returns protected builds' do
- expect(subject.builds).to all(include(protected: true))
- end
- end
-
- context 'when a ref is unprotected' do
- before do
- allow_any_instance_of(Project).to receive(:protected_for?).and_return(false)
- end
-
- it 'returns unprotected builds' do
- expect(subject.builds).to all(include(protected: false))
- end
- end
- end
-
- describe '#user=' do
- let(:user) { build(:user) }
-
- it 'assignes relevant pipeline attributes' do
- subject.user = user
-
- expect(subject.builds).to all(include(user: user))
- end
- end
-
- describe '#create!' do
- it 'creates all stages and builds' do
- subject.create!
-
- expect(pipeline.reload.stages.count).to eq 1
- expect(pipeline.reload.builds.count).to eq 2
- expect(pipeline.builds).to all(satisfy { |job| job.stage_id.present? })
- expect(pipeline.builds).to all(satisfy { |job| job.pipeline.present? })
- expect(pipeline.builds).to all(satisfy { |job| job.project.present? })
- expect(pipeline.stages)
- .to all(satisfy { |stage| stage.pipeline.present? })
- expect(pipeline.stages)
- .to all(satisfy { |stage| stage.project.present? })
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/trace/http_io_spec.rb b/spec/lib/gitlab/ci/trace/http_io_spec.rb
new file mode 100644
index 00000000000..5474e2f518c
--- /dev/null
+++ b/spec/lib/gitlab/ci/trace/http_io_spec.rb
@@ -0,0 +1,315 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Trace::HttpIO do
+ include HttpIOHelpers
+
+ let(:http_io) { described_class.new(url, size) }
+ let(:url) { remote_trace_url }
+ let(:size) { remote_trace_size }
+
+ describe '#close' do
+ subject { http_io.close }
+
+ it { is_expected.to be_nil }
+ end
+
+ describe '#binmode' do
+ subject { http_io.binmode }
+
+ it { is_expected.to be_nil }
+ end
+
+ describe '#binmode?' do
+ subject { http_io.binmode? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#path' do
+ subject { http_io.path }
+
+ it { is_expected.to be_nil }
+ end
+
+ describe '#url' do
+ subject { http_io.url }
+
+ it { is_expected.to eq(url) }
+ end
+
+ describe '#seek' do
+ subject { http_io.seek(pos, where) }
+
+ context 'when moves pos to end of the file' do
+ let(:pos) { 0 }
+ let(:where) { IO::SEEK_END }
+
+ it { is_expected.to eq(size) }
+ end
+
+ context 'when moves pos to middle of the file' do
+ let(:pos) { size / 2 }
+ let(:where) { IO::SEEK_SET }
+
+ it { is_expected.to eq(size / 2) }
+ end
+
+ context 'when moves pos around' do
+ it 'matches the result' do
+ expect(http_io.seek(0)).to eq(0)
+ expect(http_io.seek(100, IO::SEEK_CUR)).to eq(100)
+ expect { http_io.seek(size + 1, IO::SEEK_CUR) }.to raise_error('new position is outside of file')
+ end
+ end
+ end
+
+ describe '#eof?' do
+ subject { http_io.eof? }
+
+ context 'when current pos is at end of the file' do
+ before do
+ http_io.seek(size, IO::SEEK_SET)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when current pos is not at end of the file' do
+ before do
+ http_io.seek(0, IO::SEEK_SET)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#each_line' do
+ subject { http_io.each_line }
+
+ let(:string_io) { StringIO.new(remote_trace_body) }
+
+ before do
+ stub_remote_trace_206
+ end
+
+ it 'yields lines' do
+ expect { |b| http_io.each_line(&b) }.to yield_successive_args(*string_io.each_line.to_a)
+ end
+
+ context 'when buckets on GCS' do
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ stub_remote_trace_200
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'calls get_chunk only once' do
+ expect_any_instance_of(Net::HTTP).to receive(:request).once.and_call_original
+
+ http_io.each_line { |line| }
+ end
+ end
+ end
+ end
+
+ describe '#read' do
+ subject { http_io.read(length) }
+
+ context 'when there are no network issue' do
+ before do
+ stub_remote_trace_206
+ end
+
+ context 'when read whole size' do
+ let(:length) { nil }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body)
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body)
+ end
+ end
+ end
+
+ context 'when read only first 100 bytes' do
+ let(:length) { 100 }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body[0, length])
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body[0, length])
+ end
+ end
+ end
+
+ context 'when tries to read oversize' do
+ let(:length) { size + 1000 }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body)
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to eq(remote_trace_body)
+ end
+ end
+ end
+
+ context 'when tries to read 0 bytes' do
+ let(:length) { 0 }
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it 'reads a trace' do
+ is_expected.to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when there is anetwork issue' do
+ let(:length) { nil }
+
+ before do
+ stub_remote_trace_500
+ end
+
+ it 'reads a trace' do
+ expect { subject }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError)
+ end
+ end
+ end
+
+ describe '#readline' do
+ subject { http_io.readline }
+
+ let(:string_io) { StringIO.new(remote_trace_body) }
+
+ before do
+ stub_remote_trace_206
+ end
+
+ shared_examples 'all line matching' do
+ it 'reads a line' do
+ (0...remote_trace_body.lines.count).each do
+ expect(http_io.readline).to eq(string_io.readline)
+ end
+ end
+ end
+
+ context 'when there is anetwork issue' do
+ let(:length) { nil }
+
+ before do
+ stub_remote_trace_500
+ end
+
+ it 'reads a trace' do
+ expect { subject }.to raise_error(Gitlab::Ci::Trace::HttpIO::FailedToGetChunkError)
+ end
+ end
+
+ context 'when BUFFER_SIZE is smaller than file size' do
+ before do
+ set_smaller_buffer_size_than(size)
+ end
+
+ it_behaves_like 'all line matching'
+ end
+
+ context 'when BUFFER_SIZE is larger than file size' do
+ before do
+ set_larger_buffer_size_than(size)
+ end
+
+ it_behaves_like 'all line matching'
+ end
+
+ context 'when pos is at middle of the file' do
+ before do
+ set_smaller_buffer_size_than(size)
+
+ http_io.seek(size / 2)
+ string_io.seek(size / 2)
+ end
+
+ it 'reads from pos' do
+ expect(http_io.readline).to eq(string_io.readline)
+ end
+ end
+ end
+
+ describe '#write' do
+ subject { http_io.write(nil) }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#truncate' do
+ subject { http_io.truncate(nil) }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#flush' do
+ subject { http_io.flush }
+
+ it { expect { subject }.to raise_error(NotImplementedError) }
+ end
+
+ describe '#present?' do
+ subject { http_io.present? }
+
+ it { is_expected.to be_truthy }
+ end
+end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 448c6fb57dd..3a9371ed2e8 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -510,6 +510,28 @@ describe Gitlab::Ci::Trace do
it_behaves_like 'source trace in database stays intact', error: ActiveRecord::RecordInvalid
end
+
+ context 'when there is a validation error on Ci::Build' do
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::Build).to receive_message_chain(:errors, :full_messages)
+ .and_return(%w[Error Error])
+ end
+
+ context "when erase old trace with 'save'" do
+ before do
+ build.send(:write_attribute, :trace, nil)
+ build.save
+ end
+
+ it 'old trace is not deleted' do
+ build.reload
+ expect(build.trace.raw).to eq(trace_content)
+ end
+ end
+
+ it_behaves_like 'archive trace in database'
+ end
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index f83f932e61e..fbc2af29b98 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -18,6 +18,34 @@ module Gitlab
describe '#build_attributes' do
subject { described_class.new(config).build_attributes(:rspec) }
+ describe 'attributes list' do
+ let(:config) do
+ YAML.dump(
+ before_script: ['pwd'],
+ rspec: { script: 'rspec' }
+ )
+ end
+
+ it 'returns valid build attributes' do
+ expect(subject).to eq({
+ stage: "test",
+ stage_idx: 1,
+ name: "rspec",
+ commands: "pwd\nrspec",
+ coverage_regex: nil,
+ tag_list: [],
+ options: {
+ before_script: ["pwd"],
+ script: ["rspec"]
+ },
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ yaml_variables: []
+ })
+ end
+ end
+
describe 'coverage entry' do
describe 'code coverage regexp' do
let(:config) do
@@ -105,512 +133,118 @@ module Gitlab
end
end
- describe '#stage_seeds' do
- context 'when no refs policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod' },
- rspec: { stage: 'test', script: 'rspec' },
- spinach: { stage: 'test', script: 'spinach' })
- end
-
- let(:pipeline) { create(:ci_empty_pipeline) }
-
- it 'correctly fabricates a stage seeds object' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 2
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.second.stage[:name]).to eq 'deploy'
- expect(seeds.first.builds.dig(0, :name)).to eq 'rspec'
- expect(seeds.first.builds.dig(1, :name)).to eq 'spinach'
- expect(seeds.second.builds.dig(0, :name)).to eq 'production'
- end
- end
-
- context 'when refs policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
- spinach: { stage: 'test', script: 'spinach', only: ['tags'] })
- end
-
- let(:pipeline) do
- create(:ci_empty_pipeline, ref: 'feature', tag: true)
- end
-
- it 'returns stage seeds only assigned to master to master' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
- end
-
- context 'when source policy is specified' do
- let(:config) do
- YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
- spinach: { stage: 'test', script: 'spinach', only: ['schedules'] })
- end
-
- let(:pipeline) do
- create(:ci_empty_pipeline, source: :schedule)
- end
-
- it 'returns stage seeds only assigned to schedules' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.stage[:name]).to eq 'test'
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
+ describe '#stages_attributes' do
+ let(:config) do
+ YAML.dump(
+ rspec: { script: 'rspec', stage: 'test', only: ['branches'] },
+ prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] }
+ )
end
- context 'when kubernetes policy is specified' do
- let(:config) do
- YAML.dump(
- spinach: { stage: 'test', script: 'spinach' },
- production: {
- stage: 'deploy',
- script: 'cap',
- only: { kubernetes: 'active' }
- }
- )
- end
-
- context 'when kubernetes is active' do
- shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
- it 'returns seeds for kubernetes dependent job' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 2
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- expect(seeds.second.builds.dig(0, :name)).to eq 'production'
- end
- end
-
- context 'when user configured kubernetes from Integration > Kubernetes' do
- let(:project) { create(:kubernetes_project) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
-
- it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
- end
-
- context 'when user configured kubernetes from CI/CD > Clusters' do
- let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
-
- it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
- end
- end
-
- context 'when kubernetes is not active' do
- it 'does not return seeds for kubernetes dependent job' do
- seeds = subject.stage_seeds(pipeline)
-
- expect(seeds.size).to eq 1
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- end
- end
+ let(:attributes) do
+ [{ name: "build",
+ index: 0,
+ builds: [] },
+ { name: "test",
+ index: 1,
+ builds:
+ [{ stage_idx: 1,
+ stage: "test",
+ commands: "rspec",
+ tag_list: [],
+ name: "rspec",
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ coverage_regex: nil,
+ yaml_variables: [],
+ options: { script: ["rspec"] },
+ only: { refs: ["branches"] },
+ except: {} }] },
+ { name: "deploy",
+ index: 2,
+ builds:
+ [{ stage_idx: 2,
+ stage: "deploy",
+ commands: "cap prod",
+ tag_list: [],
+ name: "prod",
+ allow_failure: false,
+ when: "on_success",
+ environment: nil,
+ coverage_regex: nil,
+ yaml_variables: [],
+ options: { script: ["cap prod"] },
+ only: { refs: ["tags"] },
+ except: {} }] }]
+ end
+
+ it 'returns stages seed attributes' do
+ expect(subject.stages_attributes).to eq attributes
end
end
- describe "#pipeline_stage_builds" do
- let(:type) { 'test' }
+ describe 'only / except policies validations' do
+ context 'when `only` has an invalid value' do
+ let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
+ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
- it "returns builds if no branch specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec" }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).first).to eq({
- stage: "test",
- stage_idx: 1,
- name: "rspec",
- commands: "pwd\nrspec",
- coverage_regex: nil,
- tag_list: [],
- options: {
- before_script: ["pwd"],
- script: ["rspec"]
- },
- allow_failure: false,
- when: "on_success",
- environment: nil,
- yaml_variables: []
- })
- end
-
- describe 'only' do
- it "does not return builds if only has another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["deploy"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
- end
-
- it "does not return builds if only has regexp with another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["/^deploy$/"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
- end
-
- it "returns builds if only has specified this branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["master"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
- end
-
- it "returns builds if only has a list of branches including specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: %w(master deploy) }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
- end
-
- it "returns builds if only has a branches keyword specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
- end
-
- it "does not return builds if only has a tags keyword" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["tags"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
- end
+ context 'when it is integer' do
+ let(:only) { 1 }
- it "returns builds if only has special keywords specified and source matches" do
- possibilities = [{ keyword: 'pushes', source: 'push' },
- { keyword: 'web', source: 'web' },
- { keyword: 'triggers', source: 'trigger' },
- { keyword: 'schedules', source: 'schedule' },
- { keyword: 'api', source: 'api' },
- { keyword: 'external', source: 'external' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1)
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only has to be either an array of conditions or a hash')
end
end
- it "does not return builds if only has special keywords specified and source doesn't match" do
- possibilities = [{ keyword: 'pushes', source: 'web' },
- { keyword: 'web', source: 'push' },
- { keyword: 'triggers', source: 'schedule' },
- { keyword: 'schedules', source: 'external' },
- { keyword: 'api', source: 'trigger' },
- { keyword: 'external', source: 'api' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ context 'when it is an array of integers' do
+ let(:only) { [1, 1] }
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0)
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only config should be an array of strings or regexps')
end
end
- it "returns builds if only has current repository path" do
- seed_pipeline = pipeline(ref: 'deploy')
-
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: {
- script: "rspec",
- type: type,
- only: ["branches@#{seed_pipeline.project_full_path}"]
- }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ context 'when it is invalid regex' do
+ let(:only) { ["/*invalid/"] }
- expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(1)
- end
-
- it "does not return builds if only has different repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches@fork"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
- end
-
- it "returns build only for specified type" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", only: %w(master deploy) },
- staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
- production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "deploy")).size).to eq(2)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "deploy")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "master")).size).to eq(1)
- end
-
- context 'for invalid value' do
- let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
- let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
-
- context 'when it is integer' do
- let(:only) { 1 }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only has to be either an array of conditions or a hash')
- end
- end
-
- context 'when it is an array of integers' do
- let(:only) { [1, 1] }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only config should be an array of strings or regexps')
- end
- end
-
- context 'when it is invalid regex' do
- let(:only) { ["/*invalid/"] }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:only config should be an array of strings or regexps')
- end
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:only config should be an array of strings or regexps')
end
end
end
- describe 'except' do
- it "returns builds if except has another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["deploy"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
- end
-
- it "returns builds if except has regexp with another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["/^deploy$/"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ context 'when `except` has an invalid value' do
+ let(:config) { { rspec: { script: "rspec", except: except } } }
+ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
- end
-
- it "does not return builds if except has specified this branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["master"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
- end
-
- it "does not return builds if except has a list of branches including specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: %w(master deploy) }
- })
+ context 'when it is integer' do
+ let(:except) { 1 }
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
- end
-
- it "does not return builds if except has a branches keyword specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
- end
-
- it "returns builds if except has a tags keyword" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["tags"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
- end
-
- it "does not return builds if except has special keywords specified and source matches" do
- possibilities = [{ keyword: 'pushes', source: 'push' },
- { keyword: 'web', source: 'web' },
- { keyword: 'triggers', source: 'trigger' },
- { keyword: 'schedules', source: 'schedule' },
- { keyword: 'api', source: 'api' },
- { keyword: 'external', source: 'external' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0)
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except has to be either an array of conditions or a hash')
end
end
- it "returns builds if except has special keywords specified and source doesn't match" do
- possibilities = [{ keyword: 'pushes', source: 'web' },
- { keyword: 'web', source: 'push' },
- { keyword: 'triggers', source: 'schedule' },
- { keyword: 'schedules', source: 'external' },
- { keyword: 'api', source: 'trigger' },
- { keyword: 'external', source: 'api' }]
-
- possibilities.each do |possibility|
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
- })
+ context 'when it is an array of integers' do
+ let(:except) { [1, 1] }
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1)
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except config should be an array of strings or regexps')
end
end
- it "does not return builds if except has current repository path" do
- seed_pipeline = pipeline(ref: 'deploy')
-
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: {
- script: "rspec",
- type: type,
- except: ["branches@#{seed_pipeline.project_full_path}"]
- }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(0)
- end
-
- it "returns builds if except has different repository path" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches@fork"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
- end
-
- it "returns build except specified type" do
- master_pipeline = pipeline(ref: 'master')
- test_pipeline = pipeline(ref: 'test')
- deploy_pipeline = pipeline(ref: 'deploy')
-
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@#{test_pipeline.project_full_path}"] },
- staging: { script: "deploy", type: "deploy", except: ["master"] },
- production: { script: "deploy", type: "deploy", except: ["master@#{master_pipeline.project_full_path}"] }
- })
-
- config_processor = Gitlab::Ci::YamlProcessor.new(config)
-
- expect(config_processor.pipeline_stage_builds("deploy", deploy_pipeline).size).to eq(2)
- expect(config_processor.pipeline_stage_builds("test", test_pipeline).size).to eq(0)
- expect(config_processor.pipeline_stage_builds("deploy", master_pipeline).size).to eq(0)
- end
-
- context 'for invalid value' do
- let(:config) { { rspec: { script: "rspec", except: except } } }
- let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+ context 'when it is invalid regex' do
+ let(:except) { ["/*invalid/"] }
- context 'when it is integer' do
- let(:except) { 1 }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:except has to be either an array of conditions or a hash')
- end
- end
-
- context 'when it is an array of integers' do
- let(:except) { [1, 1] }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:except config should be an array of strings or regexps')
- end
- end
-
- context 'when it is invalid regex' do
- let(:except) { ["/*invalid/"] }
-
- it do
- expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'jobs:rspec:except config should be an array of strings or regexps')
- end
+ it do
+ expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+ 'jobs:rspec:except config should be an array of strings or regexps')
end
end
end
@@ -620,7 +254,7 @@ module Gitlab
let(:config_data) { YAML.dump(config) }
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data) }
- subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first }
+ subject { config_processor.stage_builds_attributes('test').first }
describe "before_script" do
context "in global context" do
@@ -703,8 +337,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -738,8 +372,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -771,8 +405,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -800,8 +434,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -946,8 +580,8 @@ module Gitlab
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ builds = config_processor.stage_builds_attributes("test")
- builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master"))
expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq(when_state)
end
@@ -978,8 +612,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
@@ -997,8 +631,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
@@ -1017,8 +651,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
paths: ["test/"],
untracked: false,
key: 'local',
@@ -1046,8 +680,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
- expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
+ expect(config_processor.stage_builds_attributes("test").size).to eq(1)
+ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -1083,8 +717,8 @@ module Gitlab
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ builds = config_processor.stage_builds_attributes("test")
- builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master"))
expect(builds.size).to eq(1)
expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
end
@@ -1099,7 +733,7 @@ module Gitlab
end
let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
- let(:builds) { processor.pipeline_stage_builds('deploy', pipeline(ref: 'master')) }
+ let(:builds) { processor.stage_builds_attributes('deploy') }
context 'when a production environment is specified' do
let(:environment) { 'production' }
@@ -1256,7 +890,7 @@ module Gitlab
describe "Hidden jobs" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
- subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) }
+ subject { config_processor.stage_builds_attributes("test") }
shared_examples 'hidden_job_handling' do
it "doesn't create jobs that start with dot" do
@@ -1304,7 +938,7 @@ module Gitlab
describe "YAML Alias/Anchor" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
- subject { config_processor.pipeline_stage_builds("build", pipeline(ref: "master")) }
+ subject { config_processor.stage_builds_attributes("build") }
shared_examples 'job_templates_handling' do
it "is correctly supported for jobs" do
@@ -1344,13 +978,13 @@ module Gitlab
context 'when template is a job' do
let(:config) do
- <<EOT
-job1: &JOBTMPL
- stage: build
- script: execute-script-for-job
+ <<~EOT
+ job1: &JOBTMPL
+ stage: build
+ script: execute-script-for-job
-job2: *JOBTMPL
-EOT
+ job2: *JOBTMPL
+ EOT
end
it_behaves_like 'job_templates_handling'
@@ -1358,15 +992,15 @@ EOT
context 'when template is a hidden job' do
let(:config) do
- <<EOT
-.template: &JOBTMPL
- stage: build
- script: execute-script-for-job
+ <<~EOT
+ .template: &JOBTMPL
+ stage: build
+ script: execute-script-for-job
-job1: *JOBTMPL
+ job1: *JOBTMPL
-job2: *JOBTMPL
-EOT
+ job2: *JOBTMPL
+ EOT
end
it_behaves_like 'job_templates_handling'
@@ -1374,18 +1008,18 @@ EOT
context 'when job adds its own keys to a template definition' do
let(:config) do
- <<EOT
-.template: &JOBTMPL
- stage: build
-
-job1:
- <<: *JOBTMPL
- script: execute-script-for-job
-
-job2:
- <<: *JOBTMPL
- script: execute-script-for-job
-EOT
+ <<~EOT
+ .template: &JOBTMPL
+ stage: build
+
+ job1:
+ <<: *JOBTMPL
+ script: execute-script-for-job
+
+ job2:
+ <<: *JOBTMPL
+ script: execute-script-for-job
+ EOT
end
it_behaves_like 'job_templates_handling'
@@ -1724,10 +1358,6 @@ EOT
it { is_expected.to be_nil }
end
end
-
- def pipeline(**attributes)
- build_stubbed(:ci_empty_pipeline, **attributes)
- end
end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 1de3a14b809..280f799f2ab 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -67,17 +67,35 @@ describe Gitlab::Database::MigrationHelpers do
model.add_concurrent_index(:users, :foo, unique: true)
end
+
+ it 'does nothing if the index exists already' do
+ expect(model).to receive(:index_exists?)
+ .with(:users, :foo, { algorithm: :concurrently, unique: true }).and_return(true)
+ expect(model).not_to receive(:add_index)
+
+ model.add_concurrent_index(:users, :foo, unique: true)
+ end
end
context 'using MySQL' do
- it 'creates a regular index' do
- expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ end
+ it 'creates a regular index' do
expect(model).to receive(:add_index)
.with(:users, :foo, {})
model.add_concurrent_index(:users, :foo)
end
+
+ it 'does nothing if the index exists already' do
+ expect(model).to receive(:index_exists?)
+ .with(:users, :foo, { unique: true }).and_return(true)
+ expect(model).not_to receive(:add_index)
+
+ model.add_concurrent_index(:users, :foo, unique: true)
+ end
end
end
@@ -95,6 +113,7 @@ describe Gitlab::Database::MigrationHelpers do
context 'outside a transaction' do
before do
allow(model).to receive(:transaction_open?).and_return(false)
+ allow(model).to receive(:index_exists?).and_return(true)
end
context 'using PostgreSQL' do
@@ -103,18 +122,41 @@ describe Gitlab::Database::MigrationHelpers do
allow(model).to receive(:disable_statement_timeout)
end
- it 'removes the index concurrently by column name' do
- expect(model).to receive(:remove_index)
- .with(:users, { algorithm: :concurrently, column: :foo })
+ describe 'by column name' do
+ it 'removes the index concurrently' do
+ expect(model).to receive(:remove_index)
+ .with(:users, { algorithm: :concurrently, column: :foo })
- model.remove_concurrent_index(:users, :foo)
+ model.remove_concurrent_index(:users, :foo)
+ end
+
+ it 'does nothing if the index does not exist' do
+ expect(model).to receive(:index_exists?)
+ .with(:users, :foo, { algorithm: :concurrently, unique: true }).and_return(false)
+ expect(model).not_to receive(:remove_index)
+
+ model.remove_concurrent_index(:users, :foo, unique: true)
+ end
end
- it 'removes the index concurrently by index name' do
- expect(model).to receive(:remove_index)
- .with(:users, { algorithm: :concurrently, name: "index_x_by_y" })
+ describe 'by index name' do
+ before do
+ allow(model).to receive(:index_exists_by_name?).with(:users, "index_x_by_y").and_return(true)
+ end
+
+ it 'removes the index concurrently by index name' do
+ expect(model).to receive(:remove_index)
+ .with(:users, { algorithm: :concurrently, name: "index_x_by_y" })
+
+ model.remove_concurrent_index_by_name(:users, "index_x_by_y")
+ end
+
+ it 'does nothing if the index does not exist' do
+ expect(model).to receive(:index_exists_by_name?).with(:users, "index_x_by_y").and_return(false)
+ expect(model).not_to receive(:remove_index)
- model.remove_concurrent_index_by_name(:users, "index_x_by_y")
+ model.remove_concurrent_index_by_name(:users, "index_x_by_y")
+ end
end
end
@@ -141,6 +183,10 @@ describe Gitlab::Database::MigrationHelpers do
end
describe '#add_concurrent_foreign_key' do
+ before do
+ allow(model).to receive(:foreign_key_exists?).and_return(false)
+ end
+
context 'inside a transaction' do
it 'raises an error' do
expect(model).to receive(:transaction_open?).and_return(true)
@@ -157,14 +203,23 @@ describe Gitlab::Database::MigrationHelpers do
end
context 'using MySQL' do
- it 'creates a regular foreign key' do
+ before do
allow(Gitlab::Database).to receive(:mysql?).and_return(true)
+ end
+ it 'creates a regular foreign key' do
expect(model).to receive(:add_foreign_key)
.with(:projects, :users, column: :user_id, on_delete: :cascade)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
end
+
+ it 'does not create a foreign key if it exists already' do
+ expect(model).to receive(:foreign_key_exists?).with(:projects, :users, column: :user_id).and_return(true)
+ expect(model).not_to receive(:add_foreign_key)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end
end
context 'using PostgreSQL' do
@@ -189,6 +244,14 @@ describe Gitlab::Database::MigrationHelpers do
column: :user_id,
on_delete: :nullify)
end
+
+ it 'does not create a foreign key if it exists already' do
+ expect(model).to receive(:foreign_key_exists?).with(:projects, :users, column: :user_id).and_return(true)
+ expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/)
+ expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/)
+
+ model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
+ end
end
end
end
@@ -203,6 +266,29 @@ describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#foreign_key_exists?' do
+ before do
+ key = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(:projects, :users, { column: :non_standard_id })
+ allow(model).to receive(:foreign_keys).with(:projects).and_return([key])
+ end
+
+ it 'finds existing foreign keys by column' do
+ expect(model.foreign_key_exists?(:projects, :users, column: :non_standard_id)).to be_truthy
+ end
+
+ it 'finds existing foreign keys by target table only' do
+ expect(model.foreign_key_exists?(:projects, :users)).to be_truthy
+ end
+
+ it 'compares by column name if given' do
+ expect(model.foreign_key_exists?(:projects, :users, column: :user_id)).to be_falsey
+ end
+
+ it 'compares by target if no column given' do
+ expect(model.foreign_key_exists?(:projects, :other_table)).to be_falsey
+ end
+ end
+
describe '#disable_statement_timeout' do
context 'using PostgreSQL' do
it 'disables statement timeouts' do
@@ -1125,4 +1211,33 @@ describe Gitlab::Database::MigrationHelpers do
expect(model.perform_background_migration_inline?).to eq(false)
end
end
+
+ describe '#index_exists_by_name?' do
+ it 'returns true if an index exists' do
+ expect(model.index_exists_by_name?(:projects, 'index_projects_on_path'))
+ .to be_truthy
+ end
+
+ it 'returns false if the index does not exist' do
+ expect(model.index_exists_by_name?(:projects, 'this_does_not_exist'))
+ .to be_falsy
+ end
+
+ context 'when an index with a function exists', :postgresql do
+ before do
+ ActiveRecord::Base.connection.execute(
+ 'CREATE INDEX test_index ON projects (LOWER(path));'
+ )
+ end
+
+ after do
+ 'DROP INDEX IF EXISTS test_index;'
+ end
+
+ it 'returns true if an index exists' do
+ expect(model.index_exists_by_name?(:projects, 'test_index'))
+ .to be_truthy
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 9204ea37963..0c2e18c268a 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -455,5 +455,17 @@ describe Gitlab::Diff::File do
expect(diff_file.size).to be_zero
end
end
+
+ describe '#different_type?' do
+ it 'returns false' do
+ expect(diff_file).not_to be_different_type
+ end
+ end
+
+ describe '#content_changed?' do
+ it 'returns false' do
+ expect(diff_file).not_to be_content_changed
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 83d431a7458..e68c9850f6b 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -161,6 +161,11 @@ describe Gitlab::EncodingHelper do
'removes invalid bytes from ASCII-8bit encoded multibyte string.',
"Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'),
"Lorem ipsum\n dolor sit amet, xyàyùabcdùefg"
+ ],
+ [
+ 'handles UTF-16BE encoded strings',
+ "\xFE\xFF\x00\x41".force_encoding('ASCII-8BIT'), # An "A" prepended with UTF-16 BOM
+ "\xEF\xBB\xBFA" # An "A" prepended with UTF-8 BOM
]
].each do |description, test_string, xpect|
it description do
diff --git a/spec/lib/gitlab/git/conflict/file_spec.rb b/spec/lib/gitlab/git/conflict/file_spec.rb
new file mode 100644
index 00000000000..afed6c32af6
--- /dev/null
+++ b/spec/lib/gitlab/git/conflict/file_spec.rb
@@ -0,0 +1,50 @@
+# coding: utf-8
+require 'spec_helper'
+
+describe Gitlab::Git::Conflict::File do
+ let(:conflict) { { theirs: { path: 'foo', mode: 33188 }, ours: { path: 'foo', mode: 33188 } } }
+ let(:invalid_content) { described_class.new(nil, nil, conflict, "a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }
+ let(:valid_content) { described_class.new(nil, nil, conflict, "Espa\xC3\xB1a".force_encoding(Encoding::ASCII_8BIT)) }
+
+ describe '#lines' do
+ context 'when the content contains non-UTF-8 characters' do
+ it 'raises UnsupportedEncoding' do
+ expect { invalid_content.lines }
+ .to raise_error(described_class::UnsupportedEncoding)
+ end
+ end
+
+ context 'when the content can be converted to UTF-8' do
+ it 'sets lines to the lines' do
+ expect(valid_content.lines).to eq([{
+ full_line: 'España',
+ type: nil,
+ line_obj_index: 0,
+ line_old: 1,
+ line_new: 1
+ }])
+ end
+
+ it 'sets the type to text' do
+ expect(valid_content.type).to eq('text')
+ end
+ end
+ end
+
+ describe '#content' do
+ context 'when the content contains non-UTF-8 characters' do
+ it 'raises UnsupportedEncoding' do
+ expect { invalid_content.content }
+ .to raise_error(described_class::UnsupportedEncoding)
+ end
+ end
+
+ context 'when the content can be converted to UTF-8' do
+ it 'returns a valid UTF-8 string' do
+ expect(valid_content.content).to eq('España')
+ expect(valid_content.content).to be_valid_encoding
+ expect(valid_content.content.encoding).to eq(Encoding::UTF_8)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/conflict/parser_spec.rb b/spec/lib/gitlab/git/conflict/parser_spec.rb
index 7b035a381f1..29a1702a1c6 100644
--- a/spec/lib/gitlab/git/conflict/parser_spec.rb
+++ b/spec/lib/gitlab/git/conflict/parser_spec.rb
@@ -212,13 +212,6 @@ CONFLICT
.not_to raise_error
end
end
-
- context 'when the file contains non-UTF-8 characters' do
- it 'raises UnsupportedEncoding' do
- expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }
- .to raise_error(Gitlab::Git::Conflict::Parser::UnsupportedEncoding)
- end
- end
end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 54ada3e423f..0e315b3f49e 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -604,17 +604,20 @@ describe Gitlab::Git::Repository, seed_helper: true do
shared_examples 'returning the right branches' do
let(:head_id) { repository.rugged.head.target.oid }
let(:new_branch) { head_id }
+ let(:utf8_branch) { 'branch-é' }
before do
repository.create_branch(new_branch, 'master')
+ repository.create_branch(utf8_branch, 'master')
end
after do
repository.delete_branch(new_branch)
+ repository.delete_branch(utf8_branch)
end
it 'displays that branch' do
- expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch)
+ expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch)
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 6f07e423c1b..f8f09d29c73 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -240,14 +240,21 @@ describe Gitlab::GitAccess do
end
shared_examples 'check_project_moved' do
- it 'enqueues a redirected message' do
+ it 'enqueues a redirected message for pushing' do
push_access_check
expect(Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id)).not_to be_nil
end
+
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
end
- describe '#check_project_moved!', :clean_gitlab_redis_shared_state do
+ describe '#add_project_moved_message!', :clean_gitlab_redis_shared_state do
before do
project.add_master(user)
end
@@ -261,62 +268,18 @@ describe Gitlab::GitAccess do
end
end
- context 'when a permanent redirect and ssh protocol' do
+ context 'with a redirect and ssh protocol' do
let(:redirected_path) { 'some/other-path' }
- before do
- allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
- end
-
- it 'allows push and pull access' do
- aggregate_failures do
- expect { push_access_check }.not_to raise_error
- end
- end
-
it_behaves_like 'check_project_moved'
end
- context 'with a permanent redirect and http protocol' do
+ context 'with a redirect and http protocol' do
let(:redirected_path) { 'some/other-path' }
let(:protocol) { 'http' }
- before do
- allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
- end
-
- it 'allows_push and pull access' do
- aggregate_failures do
- expect { push_access_check }.not_to raise_error
- end
- end
-
it_behaves_like 'check_project_moved'
end
-
- context 'with a temporal redirect and ssh protocol' do
- let(:redirected_path) { 'some/other-path' }
-
- it 'blocks push and pull access' do
- aggregate_failures do
- expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /Project '#{redirected_path}' was moved to '#{project.full_path}'/)
- expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/)
-
- expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /Project '#{redirected_path}' was moved to '#{project.full_path}'/)
- expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/)
- end
- end
- end
-
- context 'with a temporal redirect and http protocol' do
- let(:redirected_path) { 'some/other-path' }
- let(:protocol) { 'http' }
-
- it 'does not allow to push and pull access' do
- expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
- expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
- end
- end
end
describe '#check_authentication_abilities!' do
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
index 872377c93d8..f03c7e3f04b 100644
--- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -58,4 +58,14 @@ describe Gitlab::GitalyClient::RemoteService do
client.update_remote_mirror(ref_name, only_branches_matching)
end
end
+
+ describe '.exists?' do
+ context "when the remote doesn't exist" do
+ let(:url) { 'https://gitlab.com/gitlab-org/ik-besta-niet-of-ik-word-geplaagd.git' }
+
+ it 'returns false' do
+ expect(described_class.exists?(url)).to be(false)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 5bedfc79dd3..1f0f1fdd7da 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -38,8 +38,12 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
expect(project)
.to receive(:wiki_repository_exists?)
.and_return(false)
+ expect(Gitlab::GitalyClient::RemoteService)
+ .to receive(:exists?)
+ .with("foo.wiki.git")
+ .and_return(true)
- expect(importer.import_wiki?).to eq(true)
+ expect(importer.import_wiki?).to be(true)
end
it 'returns false if the GitHub wiki is disabled' do
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index 4c1ca4349ea..9dcf272d25e 100644
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -26,7 +26,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
let(:storages_paths) do
{
- default: { path: tmp_dir }
+ default: Gitlab::GitalyClient::StorageSettings.new('path' => tmp_dir)
}.with_indifferent_access
end
@@ -56,7 +56,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
context 'storage points to not existing folder' do
let(:storages_paths) do
{
- default: { path: 'tmp/this/path/doesnt/exist' }
+ default: Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/this/path/doesnt/exist')
}.with_indifferent_access
end
@@ -102,7 +102,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
context 'storage points to not existing folder' do
let(:storages_paths) do
{
- default: { path: 'tmp/this/path/doesnt/exist' }
+ default: Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/this/path/doesnt/exist')
}.with_indifferent_access
end
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
new file mode 100644
index 00000000000..b0bc081a3c8
--- /dev/null
+++ b/spec/lib/gitlab/http_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Gitlab::HTTP do
+ describe 'allow_local_requests_from_hooks_and_services is' do
+ before do
+ WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success')
+ end
+
+ context 'disabled' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_hooks_and_services?).and_return(false)
+ end
+
+ it 'deny requests to localhost' do
+ expect { described_class.get('http://localhost:3003') }.to raise_error(URI::InvalidURIError)
+ end
+
+ it 'deny requests to private network' do
+ expect { described_class.get('http://192.168.1.2:3003') }.to raise_error(URI::InvalidURIError)
+ end
+
+ context 'if allow_local_requests set to true' do
+ it 'override the global value and allow requests to localhost or private network' do
+ expect { described_class.get('http://localhost:3003', allow_local_requests: true) }.not_to raise_error
+ end
+ end
+ end
+
+ context 'enabled' do
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_hooks_and_services?).and_return(true)
+ end
+
+ it 'allow requests to localhost' do
+ expect { described_class.get('http://localhost:3003') }.not_to raise_error
+ end
+
+ it 'allow requests to private network' do
+ expect { described_class.get('http://192.168.1.2:3003') }.not_to raise_error
+ end
+
+ context 'if allow_local_requests set to false' do
+ it 'override the global value and ban requests to localhost or private network' do
+ expect { described_class.get('http://localhost:3003', allow_local_requests: false) }.to raise_error(URI::InvalidURIError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index bece82e531a..a204a8f1ffe 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -279,6 +279,7 @@ project:
- lfs_file_locks
- project_badges
- source_of_merge_requests
+- internal_ids
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 0b938892da5..0716852f57f 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -265,7 +265,9 @@ CommitStatus:
- target_url
- description
- artifacts_file
+- artifacts_file_store
- artifacts_metadata
+- artifacts_metadata_store
- erased_by_id
- erased_at
- artifacts_expire_at
@@ -458,6 +460,7 @@ Project:
- merge_requests_ff_only_enabled
- merge_requests_rebase_enabled
- jobs_cache_index
+- pages_https_only
Author:
- name
ProjectFeature:
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
new file mode 100644
index 00000000000..d808b4d49e0
--- /dev/null
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::OmniauthInitializer do
+ let(:devise_config) { class_double(Devise) }
+
+ subject { described_class.new(devise_config) }
+
+ describe '#execute' do
+ it 'configures providers from array' do
+ generic_config = { 'name' => 'generic' }
+
+ expect(devise_config).to receive(:omniauth).with(:generic)
+
+ subject.execute([generic_config])
+ end
+
+ it 'allows "args" array for app_id and app_secret' do
+ legacy_config = { 'name' => 'legacy', 'args' => %w(123 abc) }
+
+ expect(devise_config).to receive(:omniauth).with(:legacy, '123', 'abc')
+
+ subject.execute([legacy_config])
+ end
+
+ it 'passes app_id and app_secret as additional arguments' do
+ twitter_config = { 'name' => 'twitter', 'app_id' => '123', 'app_secret' => 'abc' }
+
+ expect(devise_config).to receive(:omniauth).with(:twitter, '123', 'abc')
+
+ subject.execute([twitter_config])
+ end
+
+ it 'passes "args" hash as symbolized hash argument' do
+ hash_config = { 'name' => 'hash', 'args' => { 'custom' => 'format' } }
+
+ expect(devise_config).to receive(:omniauth).with(:hash, custom: 'format')
+
+ subject.execute([hash_config])
+ end
+
+ it 'configures fail_with_empty_uid for shibboleth' do
+ shibboleth_config = { 'name' => 'shibboleth', 'args' => {} }
+
+ expect(devise_config).to receive(:omniauth).with(:shibboleth, fail_with_empty_uid: true)
+
+ subject.execute([shibboleth_config])
+ end
+
+ it 'configures remote_sign_out_handler proc for authentiq' do
+ authentiq_config = { 'name' => 'authentiq', 'args' => {} }
+
+ expect(devise_config).to receive(:omniauth).with(:authentiq, remote_sign_out_handler: an_instance_of(Proc))
+
+ subject.execute([authentiq_config])
+ end
+
+ it 'configures on_single_sign_out proc for cas3' do
+ cas3_config = { 'name' => 'cas3', 'args' => {} }
+
+ expect(devise_config).to receive(:omniauth).with(:cas3, on_single_sign_out: an_instance_of(Proc))
+
+ subject.execute([cas3_config])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 3d5b56cd5b8..548eb28fe4d 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -110,8 +110,8 @@ describe Gitlab::Profiler do
custom_logger.debug('User Load (1.3ms)')
custom_logger.debug('Project Load (10.4ms)')
- expect(custom_logger.load_times_by_model).to eq('User' => 2.5,
- 'Project' => 10.4)
+ expect(custom_logger.load_times_by_model).to eq('User' => [1.2, 1.3],
+ 'Project' => [10.4])
end
it 'logs the backtrace, ignoring lines as appropriate' do
@@ -164,4 +164,24 @@ describe Gitlab::Profiler do
end
end
end
+
+ describe '.log_load_times_by_model' do
+ it 'logs the model, query count, and time by slowest first' do
+ expect(null_logger).to receive(:load_times_by_model).and_return(
+ 'User' => [1.2, 1.3],
+ 'Project' => [10.4]
+ )
+
+ expect(null_logger).to receive(:info).with('Project total (1): 10.4ms')
+ expect(null_logger).to receive(:info).with('User total (2): 2.5ms')
+
+ described_class.log_load_times_by_model(null_logger)
+ end
+
+ it 'does nothing when called with a logger that does not have load times' do
+ expect(null_logger).not_to receive(:info)
+
+ expect(described_class.log_load_times_by_model(null_logger)).to be_nil
+ end
+ end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 57905a74e92..8351b967133 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -83,19 +83,19 @@ describe Gitlab::ProjectSearchResults do
end
context 'when the matching filename contains a colon' do
- let(:search_result) { "\nmaster:testdata/project::function1.yaml\x001\x00---\n" }
+ let(:search_result) { "master:testdata/project::function1.yaml\x001\x00---\n" }
it 'returns a valid FoundBlob' do
expect(subject.filename).to eq('testdata/project::function1.yaml')
expect(subject.basename).to eq('testdata/project::function1')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
- expect(subject.data).to eq('---')
+ expect(subject.data).to eq("---\n")
end
end
context 'when the matching content contains a number surrounded by colons' do
- let(:search_result) { "\nmaster:testdata/foo.txt\x001\x00blah:9:blah" }
+ let(:search_result) { "master:testdata/foo.txt\x001\x00blah:9:blah" }
it 'returns a valid FoundBlob' do
expect(subject.filename).to eq('testdata/foo.txt')
@@ -106,6 +106,18 @@ describe Gitlab::ProjectSearchResults do
end
end
+ context 'when the search result ends with an empty line' do
+ let(:results) { project.repository.search_files_by_content('Role models', 'master') }
+
+ it 'returns a valid FoundBlob that ends with an empty line' do
+ expect(subject.filename).to eq('files/markdown/ruby-style-guide.md')
+ expect(subject.basename).to eq('files/markdown/ruby-style-guide')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("# Prelude\n\n> Role models are important. <br/>\n> -- Officer Alex J. Murphy / RoboCop\n\n")
+ end
+ end
+
context 'when the search returns non-ASCII data' do
context 'with UTF-8' do
let(:results) { project.repository.search_files_by_content('файл', 'master') }
@@ -115,7 +127,7 @@ describe Gitlab::ProjectSearchResults do
expect(subject.basename).to eq('encoding/russian')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
- expect(subject.data).to eq('Хороший файл')
+ expect(subject.data).to eq("Хороший файл\n")
end
end
@@ -139,7 +151,7 @@ describe Gitlab::ProjectSearchResults do
expect(subject.basename).to eq('encoding/iso8859')
expect(subject.ref).to eq('master')
expect(subject.startline).to eq(1)
- expect(subject.data).to eq("Äü\n\nfoo")
+ expect(subject.data).to eq("Äü\n\nfoo\n")
end
end
end
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
index b67bcc77bd4..f030f371372 100644
--- a/spec/lib/gitlab/repo_path_spec.rb
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -48,8 +48,8 @@ describe ::Gitlab::RepoPath do
describe '.strip_storage_path' do
before do
allow(Gitlab.config.repositories).to receive(:storages).and_return({
- 'storage1' => { 'path' => '/foo' },
- 'storage2' => { 'path' => '/bar' }
+ 'storage1' => Gitlab::GitalyClient::StorageSettings.new('path' => '/foo'),
+ 'storage2' => Gitlab::GitalyClient::StorageSettings.new('path' => '/bar')
})
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 14b59c5e945..ea5ce58e34b 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -405,7 +405,7 @@ describe Gitlab::Shell do
describe '#create_repository' do
shared_examples '#create_repository' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
let(:repo_name) { 'project/path' }
let(:created_path) { File.join(repository_storage_path, repo_name + '.git') }
@@ -679,7 +679,7 @@ describe Gitlab::Shell do
describe 'namespace actions' do
subject { described_class.new }
- let(:storage_path) { Gitlab.config.repositories.storages.default.path }
+ let(:storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path }
describe '#add_namespace' do
it 'creates a namespace' do
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index d9b3c2350b1..2d35b026485 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
describe Gitlab::UrlBlocker do
describe '#blocked_url?' do
+ let(:valid_ports) { Project::VALID_IMPORT_PORTS }
+
it 'allows imports from configured web host and port' do
import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git"
expect(described_class.blocked_url?(import_url)).to be false
@@ -17,7 +19,7 @@ describe Gitlab::UrlBlocker do
end
it 'returns true for bad port' do
- expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true
+ expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git', valid_ports: valid_ports)).to be true
end
it 'returns true for alternative version of 127.0.0.1 (0177.1)' do
@@ -71,6 +73,47 @@ describe Gitlab::UrlBlocker do
it 'returns false for legitimate URL' do
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
end
+
+ context 'when allow_private_networks is' do
+ let(:private_networks) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] }
+ let(:fake_domain) { 'www.fakedomain.fake' }
+
+ context 'true (default)' do
+ it 'does not block urls from private networks' do
+ private_networks.each do |ip|
+ stub_domain_resolv(fake_domain, ip)
+
+ expect(described_class).not_to be_blocked_url("http://#{fake_domain}")
+
+ unstub_domain_resolv
+
+ expect(described_class).not_to be_blocked_url("http://#{ip}")
+ end
+ end
+ end
+
+ context 'false' do
+ it 'blocks urls from private networks' do
+ private_networks.each do |ip|
+ stub_domain_resolv(fake_domain, ip)
+
+ expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_private_networks: false)
+
+ unstub_domain_resolv
+
+ expect(described_class).to be_blocked_url("http://#{ip}", allow_private_networks: false)
+ end
+ end
+ end
+
+ def stub_domain_resolv(domain, ip)
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([double(ip_address: ip, ipv4_private?: true)])
+ end
+
+ def unstub_domain_resolv
+ allow(Addrinfo).to receive(:getaddrinfo).and_call_original
+ end
+ end
end
# Resolv does not support resolving UTF-8 domain names
diff --git a/spec/lib/gitlab/verify/lfs_objects_spec.rb b/spec/lib/gitlab/verify/lfs_objects_spec.rb
index 64f3a9660e0..0f890e2c7ce 100644
--- a/spec/lib/gitlab/verify/lfs_objects_spec.rb
+++ b/spec/lib/gitlab/verify/lfs_objects_spec.rb
@@ -31,5 +31,21 @@ describe Gitlab::Verify::LfsObjects do
expect(failures.keys).to contain_exactly(lfs_object)
expect(failure.to_s).to include('Checksum mismatch')
end
+
+ context 'with remote files' do
+ before do
+ stub_lfs_object_storage
+ end
+
+ it 'skips LFS objects in object storage' do
+ local_failure = create(:lfs_object)
+ create(:lfs_object, :object_storage)
+
+ failures = {}
+ described_class.new(batch_size: 10).run_batches { |_, failed| failures.merge!(failed) }
+
+ expect(failures.keys).to contain_exactly(local_failure)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb
index 6146ce61226..85768308edc 100644
--- a/spec/lib/gitlab/verify/uploads_spec.rb
+++ b/spec/lib/gitlab/verify/uploads_spec.rb
@@ -40,5 +40,21 @@ describe Gitlab::Verify::Uploads do
expect(failures.keys).to contain_exactly(upload)
expect(failure.to_s).to include('Checksum missing')
end
+
+ context 'with remote files' do
+ before do
+ stub_uploads_object_storage(AvatarUploader)
+ end
+
+ it 'skips uploads in object storage' do
+ local_failure = create(:upload)
+ create(:upload, :object_storage)
+
+ failures = {}
+ described_class.new(batch_size: 10).run_batches { |_, failed| failures.merge!(failed) }
+
+ expect(failures.keys).to contain_exactly(local_failure)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 37a0bf1ad36..d67bfb37be7 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -55,7 +55,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_archive feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -100,7 +100,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_send_git_patch feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -173,7 +173,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_send_git_diff feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -275,14 +275,12 @@ describe Gitlab::Workhorse do
describe '.git_http_ok' do
let(:user) { create(:user) }
- let(:repo_path) { repository.path_to_repo }
let(:action) { 'info_refs' }
let(:params) do
{
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "project-#{project.id}",
- RepoPath: repo_path,
ShowAllRefs: false
}
end
@@ -297,7 +295,6 @@ describe Gitlab::Workhorse do
GL_ID: "user-#{user.id}",
GL_USERNAME: user.username,
GL_REPOSITORY: "wiki-#{project.id}",
- RepoPath: repo_path,
ShowAllRefs: false
}
end
@@ -455,7 +452,7 @@ describe Gitlab::Workhorse do
end
end
- context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly workhorse_raw_show feature is disabled', :disable_gitaly do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb
index 369e7b181b9..8ba15ae0f38 100644
--- a/spec/lib/mattermost/command_spec.rb
+++ b/spec/lib/mattermost/command_spec.rb
@@ -4,10 +4,11 @@ describe Mattermost::Command do
let(:params) { { 'token' => 'token', team_id: 'abc' } }
before do
- Mattermost::Session.base_uri('http://mattermost.example.com')
+ session = Mattermost::Session.new(nil)
+ session.base_uri = 'http://mattermost.example.com'
allow_any_instance_of(Mattermost::Client).to receive(:with_session)
- .and_yield(Mattermost::Session.new(nil))
+ .and_yield(session)
end
describe '#create' do
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 3db19d06305..c855643c4d8 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -15,7 +15,7 @@ describe Mattermost::Session, type: :request do
it { is_expected.to respond_to(:strategy) }
before do
- described_class.base_uri(mattermost_url)
+ subject.base_uri = mattermost_url
end
describe '#with session' do
diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb
index 3c8206031cf..2cfa6802612 100644
--- a/spec/lib/mattermost/team_spec.rb
+++ b/spec/lib/mattermost/team_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
describe Mattermost::Team do
before do
- Mattermost::Session.base_uri('http://mattermost.example.com')
+ session = Mattermost::Session.new(nil)
+ session.base_uri = 'http://mattermost.example.com'
allow_any_instance_of(Mattermost::Client).to receive(:with_session)
- .and_yield(Mattermost::Session.new(nil))
+ .and_yield(session)
end
describe '#all' do
diff --git a/spec/migrations/remove_dot_git_from_usernames_spec.rb b/spec/migrations/remove_dot_git_from_usernames_spec.rb
index 129374cb38c..3a88a66a476 100644
--- a/spec/migrations/remove_dot_git_from_usernames_spec.rb
+++ b/spec/migrations/remove_dot_git_from_usernames_spec.rb
@@ -29,7 +29,9 @@ describe RemoveDotGitFromUsernames do
update_namespace(user, 'test.git')
update_namespace(user2, 'test_git')
- storages = { 'default' => 'tmp/tests/custom_repositories' }
+ default_hash = Gitlab.config.repositories.storages.default.to_h
+ default_hash['path'] = 'tmp/tests/custom_repositories'
+ storages = { 'default' => Gitlab::GitalyClient::StorageSettings.new(default_hash) }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
allow(migration).to receive(:route_exists?).with('test_git').and_return(true)
diff --git a/spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb b/spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb
new file mode 100644
index 00000000000..441c4295a40
--- /dev/null
+++ b/spec/migrations/remove_empty_extern_uid_auth0_identities_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180220150310_remove_empty_extern_uid_auth0_identities.rb')
+
+describe RemoveEmptyExternUidAuth0Identities, :migration do
+ let(:identities) { table(:identities) }
+
+ before do
+ identities.create(provider: 'auth0', extern_uid: '')
+ identities.create(provider: 'auth0', extern_uid: 'valid')
+ identities.create(provider: 'github', extern_uid: '')
+
+ migrate!
+ end
+
+ it 'leaves the correct auth0 identity' do
+ expect(identities.where(provider: 'auth0').pluck(:extern_uid)).to eq(['valid'])
+ end
+
+ it 'leaves the correct github identity' do
+ expect(identities.where(provider: 'github').count).to eq(1)
+ end
+end
diff --git a/spec/migrations/remove_empty_fork_networks_spec.rb b/spec/migrations/remove_empty_fork_networks_spec.rb
index 7f7ce91378b..f6d030ab25c 100644
--- a/spec/migrations/remove_empty_fork_networks_spec.rb
+++ b/spec/migrations/remove_empty_fork_networks_spec.rb
@@ -19,6 +19,10 @@ describe RemoveEmptyForkNetworks, :migration do
deleted_project.destroy!
end
+ after do
+ Upload.reset_column_information
+ end
+
it 'deletes only the fork network without members' do
expect(fork_networks.count).to eq(2)
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 9da3de7a828..7d935cf8d76 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -198,6 +198,16 @@ describe Ci::Build do
end
context 'when legacy artifacts are used' do
+ let(:build) { create(:ci_build, :legacy_artifacts) }
+
+ subject { build.artifacts? }
+
+ context 'is expired' do
+ let(:build) { create(:ci_build, :legacy_artifacts, :expired) }
+
+ it { is_expected.to be_falsy }
+ end
+
context 'artifacts archive does not exist' do
let(:build) { create(:ci_build) }
@@ -208,13 +218,25 @@ describe Ci::Build do
let(:build) { create(:ci_build, :legacy_artifacts) }
it { is_expected.to be_truthy }
+ end
+ end
+ end
- context 'is expired' do
- let(:build) { create(:ci_build, :legacy_artifacts, :expired) }
+ describe '#browsable_artifacts?' do
+ subject { build.browsable_artifacts? }
- it { is_expected.to be_falsy }
- end
+ context 'artifacts metadata does not exist' do
+ before do
+ build.update_attributes(legacy_artifacts_metadata: nil)
end
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts metadata does exists' do
+ let(:build) { create(:ci_build, :artifacts) }
+
+ it { is_expected.to be_truthy }
end
end
@@ -2101,6 +2123,35 @@ describe Ci::Build do
subject.drop!
end
+
+ context 'when retry service raises Gitlab::Access::AccessDeniedError exception' do
+ let(:retry_service) { Ci::RetryBuildService.new(subject.project, subject.user) }
+
+ before do
+ allow_any_instance_of(Ci::RetryBuildService)
+ .to receive(:execute)
+ .with(subject)
+ .and_raise(Gitlab::Access::AccessDeniedError)
+ allow(Rails.logger).to receive(:error)
+ end
+
+ it 'handles raised exception' do
+ expect { subject.drop! }.not_to raise_exception(Gitlab::Access::AccessDeniedError)
+ end
+
+ it 'logs the error' do
+ subject.drop!
+
+ expect(Rails.logger)
+ .to have_received(:error)
+ .with(a_string_matching("Unable to auto-retry job #{subject.id}"))
+ end
+
+ it 'fails the job' do
+ subject.drop!
+ expect(subject.failed?).to be_truthy
+ end
+ end
end
context 'when build is not configured to be retried' do
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index a2bd36537e6..1aa28434879 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -15,6 +15,50 @@ describe Ci::JobArtifact do
it { is_expected.to delegate_method(:open).to(:file) }
it { is_expected.to delegate_method(:exists?).to(:file) }
+ describe 'callbacks' do
+ subject { create(:ci_job_artifact, :archive) }
+
+ describe '#schedule_background_upload' do
+ context 'when object storage is disabled' do
+ before do
+ stub_artifacts_object_storage(enabled: false)
+ end
+
+ it 'does not schedule the migration' do
+ expect(ObjectStorageUploadWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ context 'when object storage is enabled' do
+ context 'when background upload is enabled' do
+ before do
+ stub_artifacts_object_storage(background_upload: true)
+ end
+
+ it 'schedules the model for migration' do
+ expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('JobArtifactUploader', described_class.name, :file, kind_of(Numeric))
+
+ subject
+ end
+ end
+
+ context 'when background upload is disabled' do
+ before do
+ stub_artifacts_object_storage(background_upload: false)
+ end
+
+ it 'schedules the model for migration' do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+ end
+ end
+
describe '#set_size' do
it 'sets the size' do
expect(artifact.size).to eq(106365)
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 4635f8cfe9d..92f00cfbc19 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -177,6 +177,24 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#protected_ref?' do
+ it 'delegates method to project' do
+ expect(pipeline).not_to be_protected_ref
+ end
+ end
+
+ describe '#legacy_trigger' do
+ let(:trigger_request) { create(:ci_trigger_request) }
+
+ before do
+ pipeline.trigger_requests << trigger_request
+ end
+
+ it 'returns first trigger request' do
+ expect(pipeline.legacy_trigger).to eq trigger_request
+ end
+ end
+
describe '#auto_canceled?' do
subject { pipeline.auto_canceled? }
@@ -215,142 +233,257 @@ describe Ci::Pipeline, :mailer do
end
describe 'pipeline stages' do
- before do
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'linux',
- stage_idx: 0,
- status: 'success')
-
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'mac',
- stage_idx: 0,
- status: 'failed')
-
- create(:commit_status, pipeline: pipeline,
- stage: 'deploy',
- name: 'staging',
- stage_idx: 2,
- status: 'running')
-
- create(:commit_status, pipeline: pipeline,
- stage: 'test',
- name: 'rspec',
- stage_idx: 1,
- status: 'success')
- end
-
describe '#stage_seeds' do
- let(:pipeline) do
- build(:ci_pipeline, config: { rspec: { script: 'rake' } })
- end
+ let(:pipeline) { build(:ci_pipeline, config: config) }
+ let(:config) { { rspec: { script: 'rake' } } }
it 'returns preseeded stage seeds object' do
- expect(pipeline.stage_seeds).to all(be_a Gitlab::Ci::Stage::Seed)
+ expect(pipeline.stage_seeds)
+ .to all(be_a Gitlab::Ci::Pipeline::Seed::Base)
expect(pipeline.stage_seeds.count).to eq 1
end
- end
- describe '#seeds_size' do
- let(:pipeline) { build(:ci_pipeline_with_one_job) }
+ context 'when no refs policy is specified' do
+ let(:config) do
+ { production: { stage: 'deploy', script: 'cap prod' },
+ rspec: { stage: 'test', script: 'rspec' },
+ spinach: { stage: 'test', script: 'spinach' } }
+ end
- it 'returns number of jobs in stage seeds' do
- expect(pipeline.seeds_size).to eq 1
+ it 'correctly fabricates a stage seeds object' do
+ seeds = pipeline.stage_seeds
+
+ expect(seeds.size).to eq 2
+ expect(seeds.first.attributes[:name]).to eq 'test'
+ expect(seeds.second.attributes[:name]).to eq 'deploy'
+ expect(seeds.dig(0, 0, :name)).to eq 'rspec'
+ expect(seeds.dig(0, 1, :name)).to eq 'spinach'
+ expect(seeds.dig(1, 0, :name)).to eq 'production'
+ end
end
- end
- describe '#legacy_stages' do
- subject { pipeline.legacy_stages }
+ context 'when refs policy is specified' do
+ let(:pipeline) do
+ build(:ci_pipeline, ref: 'feature', tag: true, config: config)
+ end
- context 'stages list' do
- it 'returns ordered list of stages' do
- expect(subject.map(&:name)).to eq(%w[build test deploy])
+ let(:config) do
+ { production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['tags'] } }
+ end
+
+ it 'returns stage seeds only assigned to master to master' do
+ seeds = pipeline.stage_seeds
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.attributes[:name]).to eq 'test'
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
end
end
- context 'stages with statuses' do
- let(:statuses) do
- subject.map { |stage| [stage.name, stage.status] }
+ context 'when source policy is specified' do
+ let(:pipeline) { build(:ci_pipeline, source: :schedule, config: config) }
+
+ let(:config) do
+ { production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['schedules'] } }
end
- it 'returns list of stages with correct statuses' do
- expect(statuses).to eq([%w(build failed),
- %w(test success),
- %w(deploy running)])
+ it 'returns stage seeds only assigned to schedules' do
+ seeds = pipeline.stage_seeds
+
+ expect(seeds.size).to eq 1
+ expect(seeds.first.attributes[:name]).to eq 'test'
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
end
+ end
- context 'when commit status is retried' do
- before do
- create(:commit_status, pipeline: pipeline,
- stage: 'build',
- name: 'mac',
- stage_idx: 0,
- status: 'success')
+ context 'when kubernetes policy is specified' do
+ let(:config) do
+ {
+ spinach: { stage: 'test', script: 'spinach' },
+ production: {
+ stage: 'deploy',
+ script: 'cap',
+ only: { kubernetes: 'active' }
+ }
+ }
+ end
+
+ context 'when kubernetes is active' do
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns seeds for kubernetes dependent job' do
+ seeds = pipeline.stage_seeds
- pipeline.process!
+ expect(seeds.size).to eq 2
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ expect(seeds.dig(1, 0, :name)).to eq 'production'
+ end
end
- it 'ignores the previous state' do
- expect(statuses).to eq([%w(build success),
- %w(test success),
- %w(deploy running)])
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+ let(:pipeline) { build(:ci_pipeline, project: project, config: config) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
- end
- end
- context 'when there is a stage with warnings' do
- before do
- create(:commit_status, pipeline: pipeline,
- stage: 'deploy',
- name: 'prod:2',
- stage_idx: 2,
- status: 'failed',
- allow_failure: true)
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let(:pipeline) { build(:ci_pipeline, project: project, config: config) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
end
- it 'populates stage with correct number of warnings' do
- deploy_stage = pipeline.legacy_stages.third
+ context 'when kubernetes is not active' do
+ it 'does not return seeds for kubernetes dependent job' do
+ seeds = pipeline.stage_seeds
- expect(deploy_stage).not_to receive(:statuses)
- expect(deploy_stage).to have_warnings
+ expect(seeds.size).to eq 1
+ expect(seeds.dig(0, 0, :name)).to eq 'spinach'
+ end
end
end
end
- describe '#stages_count' do
- it 'returns a valid number of stages' do
- expect(pipeline.stages_count).to eq(3)
- end
- end
+ describe '#seeds_size' do
+ context 'when refs policy is specified' do
+ let(:config) do
+ { production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
+ spinach: { stage: 'test', script: 'spinach', only: ['tags'] } }
+ end
- describe '#stages_names' do
- it 'returns a valid names of stages' do
- expect(pipeline.stages_names).to eq(%w(build test deploy))
+ let(:pipeline) do
+ build(:ci_pipeline, ref: 'feature', tag: true, config: config)
+ end
+
+ it 'returns real seeds size' do
+ expect(pipeline.seeds_size).to eq 1
+ end
end
end
- end
-
- describe '#legacy_stage' do
- subject { pipeline.legacy_stage('test') }
- context 'with status in stage' do
+ describe 'legacy stages' do
before do
- create(:commit_status, pipeline: pipeline, stage: 'test')
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'linux',
+ stage_idx: 0,
+ status: 'success')
+
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'failed')
+
+ create(:commit_status, pipeline: pipeline,
+ stage: 'deploy',
+ name: 'staging',
+ stage_idx: 2,
+ status: 'running')
+
+ create(:commit_status, pipeline: pipeline,
+ stage: 'test',
+ name: 'rspec',
+ stage_idx: 1,
+ status: 'success')
+ end
+
+ describe '#legacy_stages' do
+ subject { pipeline.legacy_stages }
+
+ context 'stages list' do
+ it 'returns ordered list of stages' do
+ expect(subject.map(&:name)).to eq(%w[build test deploy])
+ end
+ end
+
+ context 'stages with statuses' do
+ let(:statuses) do
+ subject.map { |stage| [stage.name, stage.status] }
+ end
+
+ it 'returns list of stages with correct statuses' do
+ expect(statuses).to eq([%w(build failed),
+ %w(test success),
+ %w(deploy running)])
+ end
+
+ context 'when commit status is retried' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'build',
+ name: 'mac',
+ stage_idx: 0,
+ status: 'success')
+
+ pipeline.process!
+ end
+
+ it 'ignores the previous state' do
+ expect(statuses).to eq([%w(build success),
+ %w(test success),
+ %w(deploy running)])
+ end
+ end
+ end
+
+ context 'when there is a stage with warnings' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'deploy',
+ name: 'prod:2',
+ stage_idx: 2,
+ status: 'failed',
+ allow_failure: true)
+ end
+
+ it 'populates stage with correct number of warnings' do
+ deploy_stage = pipeline.legacy_stages.third
+
+ expect(deploy_stage).not_to receive(:statuses)
+ expect(deploy_stage).to have_warnings
+ end
+ end
+ end
+
+ describe '#stages_count' do
+ it 'returns a valid number of stages' do
+ expect(pipeline.stages_count).to eq(3)
+ end
end
- it { expect(subject).to be_a Ci::LegacyStage }
- it { expect(subject.name).to eq 'test' }
- it { expect(subject.statuses).not_to be_empty }
+ describe '#stages_names' do
+ it 'returns a valid names of stages' do
+ expect(pipeline.stages_names).to eq(%w(build test deploy))
+ end
+ end
end
- context 'without status in stage' do
- before do
- create(:commit_status, pipeline: pipeline, stage: 'build')
+ describe '#legacy_stage' do
+ subject { pipeline.legacy_stage('test') }
+
+ context 'with status in stage' do
+ before do
+ create(:commit_status, pipeline: pipeline, stage: 'test')
+ end
+
+ it { expect(subject).to be_a Ci::LegacyStage }
+ it { expect(subject.name).to eq 'test' }
+ it { expect(subject.statuses).not_to be_empty }
end
- it 'return stage object' do
- is_expected.to be_nil
+ context 'without status in stage' do
+ before do
+ create(:commit_status, pipeline: pipeline, stage: 'build')
+ end
+
+ it 'return stage object' do
+ is_expected.to be_nil
+ end
end
end
end
@@ -589,20 +722,6 @@ describe Ci::Pipeline, :mailer do
end
end
- describe '#has_stage_seeds?' do
- context 'when pipeline has stage seeds' do
- subject { build(:ci_pipeline_with_one_job) }
-
- it { is_expected.to have_stage_seeds }
- end
-
- context 'when pipeline does not have stage seeds' do
- subject { create(:ci_pipeline_without_jobs) }
-
- it { is_expected.not_to have_stage_seeds }
- end
- end
-
describe '#has_warnings?' do
subject { pipeline.has_warnings? }
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4b217df2e8f..f8874d14e3f 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -34,7 +34,7 @@ describe Issuable do
subject { build(:issue) }
before do
- allow(subject).to receive(:set_iid).and_return(false)
+ allow(InternalId).to receive(:generate_next).and_return(nil)
end
it { is_expected.to validate_presence_of(:project) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index abfc0896a41..d620943693c 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -240,7 +240,7 @@ describe Group do
it "is false if avatar is html page" do
group.update_attribute(:avatar, 'uploads/avatar.html')
- expect(group.avatar_type).to eq(["only images allowed"])
+ expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff"])
end
end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
new file mode 100644
index 00000000000..581fd0293cc
--- /dev/null
+++ b/spec/models/internal_id_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe InternalId do
+ let(:project) { create(:project) }
+ let(:usage) { :issues }
+ let(:issue) { build(:issue, project: project) }
+ let(:scope) { { project: project } }
+ let(:init) { ->(s) { s.project.issues.size } }
+
+ context 'validations' do
+ it { is_expected.to validate_presence_of(:usage) }
+ end
+
+ describe '.generate_next' do
+ subject { described_class.generate_next(issue, scope, usage, init) }
+
+ context 'in the absence of a record' do
+ it 'creates a record if not yet present' do
+ expect { subject }.to change { described_class.count }.from(0).to(1)
+ end
+
+ it 'stores record attributes' do
+ subject
+
+ described_class.first.tap do |record|
+ expect(record.project).to eq(project)
+ expect(record.usage).to eq(usage.to_s)
+ end
+ end
+
+ context 'with existing issues' do
+ before do
+ rand(1..10).times { create(:issue, project: project) }
+ described_class.delete_all
+ end
+
+ it 'calculates last_value values automatically' do
+ expect(subject).to eq(project.issues.size + 1)
+ end
+ end
+
+ context 'with concurrent inserts on table' do
+ it 'looks up the record if it was created concurrently' do
+ args = { **scope, usage: described_class.usages[usage.to_s] }
+ record = double
+ expect(described_class).to receive(:find_by).with(args).and_return(nil) # first call, record not present
+ expect(described_class).to receive(:find_by).with(args).and_return(record) # second call, record was created by another process
+ expect(described_class).to receive(:create!).and_raise(ActiveRecord::RecordNotUnique, 'record not unique')
+ expect(record).to receive(:increment_and_save!)
+
+ subject
+ end
+ end
+ end
+
+ it 'generates a strictly monotone, gapless sequence' do
+ seq = (0..rand(100)).map do
+ described_class.generate_next(issue, scope, usage, init)
+ end
+ normalized = seq.map { |i| i - seq.min }
+
+ expect(normalized).to eq((0..seq.size - 1).to_a)
+ end
+
+ context 'with an insufficient schema version' do
+ before do
+ described_class.reset_column_information
+ expect(ActiveRecord::Migrator).to receive(:current_version).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
+ end
+
+ let(:init) { double('block') }
+
+ it 'calculates next internal ids on the fly' do
+ val = rand(1..100)
+
+ expect(init).to receive(:call).with(issue).and_return(val)
+ expect(subject).to eq(val + 1)
+ end
+ end
+ end
+
+ describe '#increment_and_save!' do
+ let(:id) { create(:internal_id) }
+ subject { id.increment_and_save! }
+
+ it 'returns incremented iid' do
+ value = id.last_value
+
+ expect(subject).to eq(value + 1)
+ end
+
+ it 'saves the record' do
+ subject
+
+ expect(id.changed?).to be_falsey
+ end
+
+ context 'with last_value=nil' do
+ let(:id) { build(:internal_id, last_value: nil) }
+
+ it 'returns 1' do
+ expect(subject).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index feed7968f09..11154291368 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -9,11 +9,17 @@ describe Issue do
describe 'modules' do
subject { described_class }
- it { is_expected.to include_module(InternalId) }
it { is_expected.to include_module(Issuable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(:issue) }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { :issues }
+ end
end
subject { create(:issue) }
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
new file mode 100644
index 00000000000..a182116d637
--- /dev/null
+++ b/spec/models/lfs_object_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe LfsObject do
+ describe '#local_store?' do
+ it 'returns true when file_store is nil' do
+ subject.file_store = nil
+
+ expect(subject.local_store?).to eq true
+ end
+
+ it 'returns true when file_store is equal to LfsObjectUploader::Store::LOCAL' do
+ subject.file_store = LfsObjectUploader::Store::LOCAL
+
+ expect(subject.local_store?).to eq true
+ end
+
+ it 'returns false whe file_store is equal to LfsObjectUploader::Store::REMOTE' do
+ subject.file_store = LfsObjectUploader::Store::REMOTE
+
+ expect(subject.local_store?).to eq false
+ end
+ end
+
+ describe '#schedule_background_upload' do
+ before do
+ stub_lfs_setting(enabled: true)
+ end
+
+ subject { create(:lfs_object, :with_file) }
+
+ context 'when object storage is disabled' do
+ before do
+ stub_lfs_object_storage(enabled: false)
+ end
+
+ it 'does not schedule the migration' do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ context 'when object storage is enabled' do
+ context 'when background upload is enabled' do
+ context 'when is licensed' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
+
+ it 'schedules the model for migration' do
+ expect(ObjectStorage::BackgroundMoveWorker)
+ .to receive(:perform_async)
+ .with('LfsObjectUploader', described_class.name, :file, kind_of(Numeric))
+ .once
+
+ subject
+ end
+
+ it 'schedules the model for migration once' do
+ expect(ObjectStorage::BackgroundMoveWorker)
+ .to receive(:perform_async)
+ .with('LfsObjectUploader', described_class.name, :file, kind_of(Numeric))
+ .once
+
+ lfs_object = create(:lfs_object)
+ lfs_object.file = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png")
+ lfs_object.save!
+ end
+ end
+ end
+
+ context 'when background upload is disabled' do
+ before do
+ stub_lfs_object_storage(background_upload: false)
+ end
+
+ it 'schedules the model for migration' do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 4e783acbd8b..f73f44ca0ad 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -17,7 +17,7 @@ describe MergeRequest do
describe 'modules' do
subject { described_class }
- it { is_expected.to include_module(InternalId) }
+ it { is_expected.to include_module(NonatomicInternalId) }
it { is_expected.to include_module(Issuable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
@@ -1961,6 +1961,17 @@ describe MergeRequest do
expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3)
end
end
+
+ it 'runs a single query on the initial call, and none afterwards' do
+ expect { subject.merge_request_diff_for(merge_request_diff1.diff_refs) }
+ .not_to exceed_query_limit(1)
+
+ expect { subject.merge_request_diff_for(merge_request_diff2.diff_refs) }
+ .not_to exceed_query_limit(0)
+
+ expect { subject.merge_request_diff_for(merge_request_diff3.head_commit_sha) }
+ .not_to exceed_query_limit(0)
+ end
end
describe '#version_params_for' do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index ee142718f7e..62e95a622eb 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -305,7 +305,7 @@ describe Namespace do
end
describe '#rm_dir', 'callback' do
- let(:repository_storage_path) { Gitlab.config.repositories.storages.default['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path }
let(:path_in_dir) { File.join(repository_storage_path, namespace.full_path) }
let(:deleted_path) { namespace.full_path.gsub(namespace.path, "#{namespace.full_path}+#{namespace.id}+deleted") }
let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) }
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 95713d8b85b..4b85c5e8720 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -18,24 +18,63 @@ describe PagesDomain do
it { is_expected.to validate_uniqueness_of(:domain).case_insensitive }
end
- {
- 'my.domain.com' => true,
- '123.456.789' => true,
- '0x12345.com' => true,
- '0123123' => true,
- '_foo.com' => false,
- 'reserved.com' => false,
- 'a.reserved.com' => false,
- nil => false
- }.each do |value, validity|
- context "domain #{value.inspect} validity" do
- before do
- allow(Settings.pages).to receive(:host).and_return('reserved.com')
+ describe "hostname" do
+ {
+ 'my.domain.com' => true,
+ '123.456.789' => true,
+ '0x12345.com' => true,
+ '0123123' => true,
+ '_foo.com' => false,
+ 'reserved.com' => false,
+ 'a.reserved.com' => false,
+ nil => false
+ }.each do |value, validity|
+ context "domain #{value.inspect} validity" do
+ before do
+ allow(Settings.pages).to receive(:host).and_return('reserved.com')
+ end
+
+ let(:domain) { value }
+
+ it { expect(pages_domain.valid?).to eq(validity) }
+ end
+ end
+ end
+
+ describe "HTTPS-only" do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:domain) { 'my.domain.com' }
+
+ let(:project) do
+ instance_double(Project, pages_https_only?: pages_https_only)
+ end
+
+ let(:pages_domain) do
+ build(:pages_domain, certificate: certificate, key: key).tap do |pd|
+ allow(pd).to receive(:project).and_return(project)
+ pd.valid?
end
+ end
- let(:domain) { value }
+ where(:pages_https_only, :certificate, :key, :errors_on) do
+ attributes = attributes_for(:pages_domain)
+ cert, key = attributes.fetch_values(:certificate, :key)
+
+ true | nil | nil | %i(certificate key)
+ true | cert | nil | %i(key)
+ true | nil | key | %i(certificate key)
+ true | cert | key | []
+ false | nil | nil | []
+ false | cert | nil | %i(key)
+ false | nil | key | %i(key)
+ false | cert | key | []
+ end
- it { expect(pages_domain.valid?).to eq(validity) }
+ with_them do
+ it "is adds the expected errors" do
+ expect(pages_domain.errors.keys).to eq errors_on
+ end
end
end
end
@@ -43,26 +82,26 @@ describe PagesDomain do
describe 'validate certificate' do
subject { domain }
- context 'when only certificate is specified' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ context 'with matching key' do
+ let(:domain) { build(:pages_domain) }
- it { is_expected.not_to be_valid }
+ it { is_expected.to be_valid }
end
- context 'when only key is specified' do
- let(:domain) { build(:pages_domain, :with_key) }
+ context 'when no certificate is specified' do
+ let(:domain) { build(:pages_domain, :without_certificate) }
it { is_expected.not_to be_valid }
end
- context 'with matching key' do
- let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+ context 'when no key is specified' do
+ let(:domain) { build(:pages_domain, :without_key) }
- it { is_expected.to be_valid }
+ it { is_expected.not_to be_valid }
end
context 'for not matching key' do
- let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+ let(:domain) { build(:pages_domain, :with_missing_chain) }
it { is_expected.not_to be_valid }
end
@@ -103,30 +142,26 @@ describe PagesDomain do
describe '#url' do
subject { domain.url }
- context 'without the certificate' do
- let(:domain) { build(:pages_domain, certificate: '') }
+ let(:domain) { build(:pages_domain) }
- it { is_expected.to eq("http://#{domain.domain}") }
- end
+ it { is_expected.to eq("https://#{domain.domain}") }
- context 'with a certificate' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ context 'without the certificate' do
+ let(:domain) { build(:pages_domain, :without_certificate) }
- it { is_expected.to eq("https://#{domain.domain}") }
+ it { is_expected.to eq("http://#{domain.domain}") }
end
end
describe '#has_matching_key?' do
subject { domain.has_matching_key? }
- context 'for matching key' do
- let(:domain) { build(:pages_domain, :with_certificate, :with_key) }
+ let(:domain) { build(:pages_domain) }
- it { is_expected.to be_truthy }
- end
+ it { is_expected.to be_truthy }
context 'for invalid key' do
- let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) }
+ let(:domain) { build(:pages_domain, :with_missing_chain) }
it { is_expected.to be_falsey }
end
@@ -136,7 +171,7 @@ describe PagesDomain do
subject { domain.has_intermediates? }
context 'for self signed' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ let(:domain) { build(:pages_domain) }
it { is_expected.to be_truthy }
end
@@ -162,7 +197,7 @@ describe PagesDomain do
subject { domain.expired? }
context 'for valid' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ let(:domain) { build(:pages_domain) }
it { is_expected.to be_falsey }
end
@@ -175,7 +210,7 @@ describe PagesDomain do
end
describe '#subject' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ let(:domain) { build(:pages_domain) }
subject { domain.subject }
@@ -183,7 +218,7 @@ describe PagesDomain do
end
describe '#certificate_text' do
- let(:domain) { build(:pages_domain, :with_certificate) }
+ let(:domain) { build(:pages_domain) }
subject { domain.certificate_text }
@@ -191,6 +226,18 @@ describe PagesDomain do
it { is_expected.not_to be_empty }
end
+ describe "#https?" do
+ context "when a certificate is present" do
+ subject { build(:pages_domain) }
+ it { is_expected.to be_https }
+ end
+
+ context "when no certificate is present" do
+ subject { build(:pages_domain, :without_certificate) }
+ it { is_expected.not_to be_https }
+ end
+ end
+
describe '#update_daemon' do
it 'runs when the domain is created' do
domain = build(:pages_domain)
@@ -267,29 +314,30 @@ describe PagesDomain do
end
context 'TLS configuration' do
- set(:domain_with_tls) { create(:pages_domain, :with_key, :with_certificate) }
+ set(:domain_without_tls) { create(:pages_domain, :without_certificate, :without_key) }
+ set(:domain) { create(:pages_domain) }
- let(:cert1) { domain_with_tls.certificate }
+ let(:cert1) { domain.certificate }
let(:cert2) { cert1 + ' ' }
- let(:key1) { domain_with_tls.key }
+ let(:key1) { domain.key }
let(:key2) { key1 + ' ' }
it 'updates when added' do
- expect(domain).to receive(:update_daemon)
+ expect(domain_without_tls).to receive(:update_daemon)
- domain.update!(key: key1, certificate: cert1)
+ domain_without_tls.update!(key: key1, certificate: cert1)
end
it 'updates when changed' do
- expect(domain_with_tls).to receive(:update_daemon)
+ expect(domain).to receive(:update_daemon)
- domain_with_tls.update!(key: key2, certificate: cert2)
+ domain.update!(key: key2, certificate: cert2)
end
it 'updates when removed' do
- expect(domain_with_tls).to receive(:update_daemon)
+ expect(domain).to receive(:update_daemon)
- domain_with_tls.update!(key: nil, certificate: nil)
+ domain.update!(key: nil, certificate: nil)
end
end
end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index a5bdf9a9337..05d33cd3874 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -9,10 +9,11 @@ describe MattermostSlashCommandsService do
let(:user) { create(:user) }
before do
- Mattermost::Session.base_uri("http://mattermost.example.com")
+ session = Mattermost::Session.new(nil)
+ session.base_uri = 'http://mattermost.example.com'
allow_any_instance_of(Mattermost::Client).to receive(:with_session)
- .and_yield(Mattermost::Session.new(nil))
+ .and_yield(session)
end
describe '#configure' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4cf8d861595..1a7a6e035ea 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -922,7 +922,7 @@ describe Project do
it 'is false if avatar is html page' do
project.update_attribute(:avatar, 'uploads/avatar.html')
- expect(project.avatar_type).to eq(['only images allowed'])
+ expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff'])
end
end
@@ -1101,8 +1101,8 @@ describe Project do
before do
storages = {
- 'default' => { 'path' => 'tmp/tests/repositories' },
- 'picked' => { 'path' => 'tmp/tests/repositories' }
+ 'default' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories'),
+ 'picked' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/repositories')
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
@@ -3479,4 +3479,49 @@ describe Project do
end
end
end
+
+ describe "#pages_https_only?" do
+ subject { build(:project) }
+
+ context "when HTTPS pages are disabled" do
+ it { is_expected.not_to be_pages_https_only }
+ end
+
+ context "when HTTPS pages are enabled", :https_pages_enabled do
+ it { is_expected.to be_pages_https_only }
+ end
+ end
+
+ describe "#pages_https_only? validation", :https_pages_enabled do
+ subject(:project) do
+ # set-up dirty object:
+ create(:project, pages_https_only: false).tap do |p|
+ p.pages_https_only = true
+ end
+ end
+
+ context "when no domains are associated" do
+ it { is_expected.to be_valid }
+ end
+
+ context "when domains including keys and certificates are associated" do
+ before do
+ allow(project)
+ .to receive(:pages_domains)
+ .and_return([instance_double(PagesDomain, https?: true)])
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context "when domains including no keys or certificates are associated" do
+ before do
+ allow(project)
+ .to receive(:pages_domains)
+ .and_return([instance_double(PagesDomain, https?: false)])
+ end
+
+ it { is_expected.not_to be_valid }
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 5bc972bca14..60ab52565cb 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -501,28 +501,6 @@ describe Repository do
end
end
- describe '#create_hooks' do
- let(:hook_path) { File.join(repository.path_to_repo, 'hooks') }
-
- it 'symlinks the global hooks directory' do
- repository.create_hooks
-
- expect(File.symlink?(hook_path)).to be true
- expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
- end
-
- it 'replaces existing symlink with the right directory' do
- FileUtils.mkdir_p(hook_path)
-
- expect(File.symlink?(hook_path)).to be false
-
- repository.create_hooks
-
- expect(File.symlink?(hook_path)).to be true
- expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
- end
- end
-
describe "#create_dir" do
it "commits a change that creates a new directory" do
expect do
@@ -895,7 +873,7 @@ describe Repository do
end
it 'returns nil when the content is not recognizable' do
- repository.create_file(user, 'LICENSE', 'Copyright!',
+ repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to be_nil
@@ -939,7 +917,7 @@ describe Repository do
end
it 'returns nil when the content is not recognizable' do
- repository.create_file(user, 'LICENSE', 'Copyright!',
+ repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license).to be_nil
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index dfac82b327a..01238a89a81 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -16,66 +16,6 @@ describe Route do
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path).case_insensitive }
-
- describe '#ensure_permanent_paths' do
- context 'when the route is not yet persisted' do
- let(:new_route) { described_class.new(path: 'foo', source: build(:group)) }
-
- context 'when permanent conflicting redirects exist' do
- it 'is invalid' do
- redirect = build(:redirect_route, :permanent, path: 'foo/bar/baz')
- redirect.save!(validate: false)
-
- expect(new_route.valid?).to be_falsey
- expect(new_route.errors.first[1]).to eq('has been taken before')
- end
- end
-
- context 'when no permanent conflicting redirects exist' do
- it 'is valid' do
- expect(new_route.valid?).to be_truthy
- end
- end
- end
-
- context 'when path has changed' do
- before do
- route.path = 'foo'
- end
-
- context 'when permanent conflicting redirects exist' do
- it 'is invalid' do
- redirect = build(:redirect_route, :permanent, path: 'foo/bar/baz')
- redirect.save!(validate: false)
-
- expect(route.valid?).to be_falsey
- expect(route.errors.first[1]).to eq('has been taken before')
- end
- end
-
- context 'when no permanent conflicting redirects exist' do
- it 'is valid' do
- expect(route.valid?).to be_truthy
- end
- end
- end
-
- context 'when path has not changed' do
- context 'when permanent conflicting redirects exist' do
- it 'is valid' do
- redirect = build(:redirect_route, :permanent, path: 'git_lab/foo/bar')
- redirect.save!(validate: false)
-
- expect(route.valid?).to be_truthy
- end
- end
- context 'when no permanent conflicting redirects exist' do
- it 'is valid' do
- expect(route.valid?).to be_truthy
- end
- end
- end
- end
end
describe 'callbacks' do
@@ -211,43 +151,31 @@ describe Route do
end
context 'when the source is a Project' do
- it 'creates a temporal RedirectRoute' do
+ it 'creates a RedirectRoute' do
project = create(:project)
route = project.route
redirect_route = route.create_redirect('foo')
- expect(redirect_route.permanent?).to be_falsy
+ expect(redirect_route).not_to be_nil
end
end
context 'when the source is not a project' do
- it 'creates a permanent RedirectRoute' do
- redirect_route = route.create_redirect('foo', permanent: true)
- expect(redirect_route.permanent?).to be_truthy
+ it 'creates a RedirectRoute' do
+ redirect_route = route.create_redirect('foo')
+ expect(redirect_route).not_to be_nil
end
end
end
describe '#delete_conflicting_redirects' do
- context 'with permanent redirect' do
- it 'does not delete the redirect' do
- route.create_redirect("#{route.path}/foo", permanent: true)
-
- expect do
- route.delete_conflicting_redirects
- end.not_to change { RedirectRoute.count }
- end
- end
-
- context 'with temporal redirect' do
- let(:route) { create(:project).route }
+ let(:route) { create(:project).route }
- it 'deletes the redirect' do
- route.create_redirect("#{route.path}/foo")
+ it 'deletes the redirect' do
+ route.create_redirect("#{route.path}/foo")
- expect do
- route.delete_conflicting_redirects
- end.to change { RedirectRoute.count }.by(-1)
- end
+ expect do
+ route.delete_conflicting_redirects
+ end.to change { RedirectRoute.count }.by(-1)
end
context 'when a redirect route with the same path exists' do
@@ -289,31 +217,18 @@ describe Route do
end
describe '#conflicting_redirects' do
+ let(:route) { create(:project).route }
+
it 'returns an ActiveRecord::Relation' do
expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
end
- context 'with permanent redirects' do
- it 'does not return anything' do
- route.create_redirect("#{route.path}/foo", permanent: true)
- route.create_redirect("#{route.path}/foo/bar", permanent: true)
- route.create_redirect("#{route.path}/baz/quz", permanent: true)
+ it 'returns the redirect routes' do
+ redirect1 = route.create_redirect("#{route.path}/foo")
+ redirect2 = route.create_redirect("#{route.path}/foo/bar")
+ redirect3 = route.create_redirect("#{route.path}/baz/quz")
- expect(route.conflicting_redirects).to be_empty
- end
- end
-
- context 'with temporal redirects' do
- let(:route) { create(:project).route }
-
- it 'returns the redirect routes' do
- route = create(:project).route
- redirect1 = route.create_redirect("#{route.path}/foo")
- redirect2 = route.create_redirect("#{route.path}/foo/bar")
- redirect3 = route.create_redirect("#{route.path}/baz/quz")
-
- expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3])
- end
+ expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3])
end
context 'when a redirect route with the same path exists' do
@@ -348,44 +263,6 @@ describe Route do
end
end
- describe "#conflicting_redirect_exists?" do
- context 'when a conflicting redirect exists' do
- let(:group1) { create(:group, path: 'foo') }
- let(:group2) { create(:group, path: 'baz') }
-
- it 'should not be saved' do
- group1.path = 'bar'
- group1.save
-
- group2.path = 'foo'
-
- expect(group2.save).to be_falsy
- end
-
- it 'should return an error on path' do
- group1.path = 'bar'
- group1.save
-
- group2.path = 'foo'
- group2.valid?
- expect(group2.errors[:path]).to eq(['has been taken before'])
- end
- end
-
- context 'when a conflicting redirect does not exist' do
- let(:project1) { create(:project, path: 'foo') }
- let(:project2) { create(:project, path: 'baz') }
-
- it 'should be saved' do
- project1.path = 'bar'
- project1.save
-
- project2.path = 'foo'
- expect(project2.save).to be_truthy
- end
- end
- end
-
describe '#delete_conflicting_orphaned_routes' do
context 'when there is a conflicting route' do
let!(:conflicting_group) { create(:group, path: 'foo') }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 5680eb24985..bbfdda23a31 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -126,23 +126,6 @@ describe User do
end
end
- context 'when the username was used by another user before' do
- let(:username) { 'foo' }
- let!(:other_user) { create(:user, username: username) }
-
- before do
- other_user.username = 'bar'
- other_user.save!
- end
-
- it 'is invalid' do
- user = build(:user, username: username)
-
- expect(user).not_to be_valid
- expect(user.errors.full_messages).to eq(['Username has been taken before'])
- end
- end
-
context 'when the username is in use by another user' do
let(:username) { 'foo' }
let!(:other_user) { create(:user, username: username) }
@@ -1222,7 +1205,7 @@ describe User do
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
- expect(user.avatar_type).to eq(['only images allowed'])
+ expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff'])
end
end
@@ -2699,27 +2682,19 @@ describe User do
end
end
- describe "#username_previously_taken?" do
- let(:user1) { create(:user, username: 'foo') }
+ context 'changing a username' do
+ let(:user) { create(:user, username: 'foo') }
- context 'when the username has been taken before' do
- before do
- user1.username = 'bar'
- user1.save!
- end
-
- it 'should raise an ActiveRecord::RecordInvalid exception' do
- user2 = build(:user, username: 'foo')
- expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Username has been taken before/)
- end
+ it 'creates a redirect route' do
+ expect { user.update!(username: 'bar') }
+ .to change { RedirectRoute.where(path: 'foo').count }.by(1)
end
- context 'when the username has not been taken before' do
- it 'should be valid' do
- expect(RedirectRoute.count).to eq(0)
- user2 = build(:user, username: 'baz')
- expect(user2).to be_valid
- end
+ it 'deletes the redirect when a user with the old username was created' do
+ user.update!(username: 'bar')
+
+ expect { create(:user, username: 'foo') }
+ .to change { RedirectRoute.where(path: 'foo').count }.by(-1)
end
end
end
diff --git a/spec/policies/protected_branch_policy_spec.rb b/spec/policies/protected_branch_policy_spec.rb
new file mode 100644
index 00000000000..b39de42d721
--- /dev/null
+++ b/spec/policies/protected_branch_policy_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe ProtectedBranchPolicy do
+ let(:user) { create(:user) }
+ let(:name) { 'feature' }
+ let(:protected_branch) { create(:protected_branch, name: name) }
+ let(:project) { protected_branch.project }
+
+ subject { described_class.new(user, protected_branch) }
+
+ it 'branches can be updated via project masters' do
+ project.add_master(user)
+
+ is_expected.to be_allowed(:update_protected_branch)
+ end
+
+ it "branches can't be updated by guests" do
+ project.add_guest(user)
+
+ is_expected.to be_disallowed(:update_protected_branch)
+ end
+end
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index f8c93d91ec5..55962f345d4 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -339,7 +339,7 @@ describe ProjectPresenter do
it 'returns link to clusters page if more than one exists' do
project.add_master(user)
- create(:cluster, projects: [project])
+ create(:cluster, :production_environment, projects: [project])
create(:cluster, projects: [project])
expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: true,
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 852f67db958..8ad19e3f0f5 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1141,4 +1141,33 @@ describe API::Commits do
end
end
end
+
+ describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do
+ let!(:project) { create(:project, :repository, :private) }
+ let!(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
+ let(:commit) { merged_mr.merge_request_diff.commits.last }
+
+ it 'returns the correct merge request' do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(1)
+ expect(json_response[0]['id']).to eq(merged_mr.id)
+ end
+
+ it 'returns 403 for an unauthorized user' do
+ project.add_guest(user)
+
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'responds 404 when the commit does not exist' do
+ get api("/projects/#{project.id}/repository/commits/a7d26f00c35b/merge_requests", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index ca0aac87ba9..3cb90a1b8ef 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -447,6 +447,12 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
+ expect(json_response["gitaly"]).not_to be_nil
+ expect(json_response["gitaly"]["repository"]).not_to be_nil
+ expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name)
+ expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
+ expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
+ expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
end
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 6192bbd4abb..3ffdfdc0e9a 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe API::Jobs do
+ include HttpIOHelpers
+
set(:project) do
create(:project, :repository, public_builds: false)
end
@@ -112,6 +114,7 @@ describe API::Jobs do
let(:query) { Hash.new }
before do
+ job
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
end
@@ -335,10 +338,55 @@ describe API::Jobs do
end
end
+ context 'when artifacts are stored remotely' do
+ let(:proxy_download) { false }
+
+ before do
+ stub_artifacts_object_storage(proxy_download: proxy_download)
+ end
+
+ let(:job) { create(:ci_build, pipeline: pipeline) }
+ let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
+
+ before do
+ job.reload
+
+ get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
+ end
+
+ context 'when proxy download is enabled' do
+ let(:proxy_download) { true }
+
+ it 'responds with the workhorse send-url' do
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
+ end
+ end
+
+ context 'when proxy download is disabled' do
+ it 'returns location redirect' do
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'authorized user' do
+ it 'returns the file remote URL' do
+ expect(response).to redirect_to(artifact.file.url)
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job artifacts' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
it 'does not return job artifacts if not uploaded' do
get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
end
@@ -349,6 +397,7 @@ describe API::Jobs do
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
before do
+ stub_artifacts_object_storage
job.success
end
@@ -412,9 +461,24 @@ describe API::Jobs do
"attachment; filename=#{job.artifacts_file.filename}" }
end
- it { expect(response).to have_gitlab_http_status(200) }
+ it { expect(response).to have_http_status(:ok) }
it { expect(response.headers).to include(download_headers) }
end
+
+ context 'when artifacts are stored remotely' do
+ let(:job) { create(:ci_build, pipeline: pipeline, user: api_user) }
+ let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
+
+ before do
+ job.reload
+
+ get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
+ end
+
+ it 'returns location redirect' do
+ expect(response).to have_http_status(:found)
+ end
+ end
end
context 'with regular branch' do
@@ -451,6 +515,22 @@ describe API::Jobs do
end
context 'authorized user' do
+ context 'when trace is in ObjectStorage' do
+ let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+ before do
+ stub_remote_trace_206
+ allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
+ allow_any_instance_of(JobArtifactUploader).to receive(:url) { remote_trace_url }
+ allow_any_instance_of(JobArtifactUploader).to receive(:size) { remote_trace_size }
+ end
+
+ it 'returns specific job trace' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to eq(job.trace.raw)
+ end
+ end
+
context 'when trace is artifact' do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index dc3a116c060..a9ccbb32666 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -1,17 +1,17 @@
require 'rails_helper'
describe API::PagesDomains do
- set(:project) { create(:project, path: 'my.project') }
+ set(:project) { create(:project, path: 'my.project', pages_https_only: false) }
set(:user) { create(:user) }
set(:admin) { create(:admin) }
- set(:pages_domain) { create(:pages_domain, domain: 'www.domain.test', project: project) }
- set(:pages_domain_secure) { create(:pages_domain, :with_certificate, :with_key, domain: 'ssl.domain.test', project: project) }
- set(:pages_domain_expired) { create(:pages_domain, :with_expired_certificate, :with_key, domain: 'expired.domain.test', project: project) }
+ set(:pages_domain) { create(:pages_domain, :without_key, :without_certificate, domain: 'www.domain.test', project: project) }
+ set(:pages_domain_secure) { create(:pages_domain, domain: 'ssl.domain.test', project: project) }
+ set(:pages_domain_expired) { create(:pages_domain, :with_expired_certificate, domain: 'expired.domain.test', project: project) }
- let(:pages_domain_params) { build(:pages_domain, domain: 'www.other-domain.test').slice(:domain) }
- let(:pages_domain_secure_params) { build(:pages_domain, :with_certificate, :with_key, domain: 'ssl.other-domain.test', project: project).slice(:domain, :certificate, :key) }
- let(:pages_domain_secure_key_missmatch_params) {build(:pages_domain, :with_trusted_chain, :with_key, project: project).slice(:domain, :certificate, :key) }
+ let(:pages_domain_params) { build(:pages_domain, :without_key, :without_certificate, domain: 'www.other-domain.test').slice(:domain) }
+ let(:pages_domain_secure_params) { build(:pages_domain, domain: 'ssl.other-domain.test', project: project).slice(:domain, :certificate, :key) }
+ let(:pages_domain_secure_key_missmatch_params) {build(:pages_domain, :with_trusted_chain, project: project).slice(:domain, :certificate, :key) }
let(:pages_domain_secure_missing_chain_params) {build(:pages_domain, :with_missing_chain, project: project).slice(:certificate) }
let(:route) { "/projects/#{project.id}/pages/domains" }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index cee93f6ed14..d73a42f48ad 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1718,6 +1718,12 @@ describe API::Projects do
group
end
+ let(:group3) do
+ group = create(:group, name: 'group3_name', parent: group2)
+ group.add_owner(user2)
+ group
+ end
+
before do
project.add_reporter(user2)
end
@@ -1813,6 +1819,15 @@ describe API::Projects do
expect(json_response['namespace']['name']).to eq(group2.name)
end
+ it 'forks to owned subgroup' do
+ full_path = "#{group2.path}/#{group3.path}"
+ post api("/projects/#{project.id}/fork", user2), namespace: full_path
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['namespace']['name']).to eq(group3.name)
+ expect(json_response['namespace']['full_path']).to eq(full_path)
+ end
+
it 'fails to fork to not owned group' do
post api("/projects/#{project.id}/fork", user2), namespace: group.name
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index 1d23e023bb6..576fde46615 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -193,6 +193,19 @@ describe API::ProtectedBranches do
expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
end
end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ post post_endpoint, name: branch_name
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
end
context 'when authenticated as a guest' do
@@ -209,18 +222,20 @@ describe API::ProtectedBranches do
end
describe "DELETE /projects/:id/protected_branches/unprotect/:branch" do
+ let(:delete_endpoint) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) }
+
before do
project.add_master(user)
end
it "unprotects a single branch" do
- delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user)
+ delete delete_endpoint
expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) }
+ let(:request) { delete_endpoint }
end
it "returns 404 if branch does not exist" do
@@ -229,11 +244,24 @@ describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(404)
end
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ delete delete_endpoint
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
context 'when branch has a wildcard in its name' do
let(:protected_name) { 'feature*' }
it "unprotects a wildcard branch" do
- delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user)
+ delete delete_endpoint
expect(response).to have_gitlab_http_status(204)
end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 95c23726a79..f3dd121faa9 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -200,7 +200,7 @@ describe API::Runner do
let(:project) { create(:project, shared_runners_enabled: false) }
let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
let(:runner) { create(:ci_runner) }
- let!(:job) do
+ let(:job) do
create(:ci_build, :artifacts, :extended_options,
pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, commands: "ls\ndate")
end
@@ -215,6 +215,7 @@ describe API::Runner do
let(:user_agent) { 'gitlab-runner 9.0.0 (9-0-stable; go1.7.4; linux/amd64)' }
before do
+ job
stub_container_registry_config(enabled: false)
end
@@ -888,6 +889,7 @@ describe API::Runner do
let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
before do
+ stub_artifacts_object_storage
job.run!
end
@@ -1179,27 +1181,67 @@ describe API::Runner do
describe 'GET /api/v4/jobs/:id/artifacts' do
let(:token) { job.token }
- before do
- download_artifact
- end
-
context 'when job has artifacts' do
- let(:job) { create(:ci_build, :artifacts) }
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ let(:job) { create(:ci_build) }
+ let(:store) { JobArtifactUploader::Store::LOCAL }
+
+ before do
+ create(:ci_job_artifact, :archive, file_store: store, job: job)
end
context 'when using job token' do
- it 'download artifacts' do
- expect(response).to have_gitlab_http_status(200)
- expect(response.headers).to include download_headers
+ context 'when artifacts are stored locally' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ end
+
+ before do
+ download_artifact
+ end
+
+ it 'download artifacts' do
+ expect(response).to have_http_status(200)
+ expect(response.headers).to include download_headers
+ end
+ end
+
+ context 'when artifacts are stored remotely' do
+ let(:store) { JobArtifactUploader::Store::REMOTE }
+ let!(:job) { create(:ci_build) }
+
+ context 'when proxy download is being used' do
+ before do
+ download_artifact(direct_download: false)
+ end
+
+ it 'uses workhorse send-url' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers).to include(
+ 'Gitlab-Workhorse-Send-Data' => /send-url:/)
+ end
+ end
+
+ context 'when direct download is being used' do
+ before do
+ download_artifact(direct_download: true)
+ end
+
+ it 'receive redirect for downloading artifacts' do
+ expect(response).to have_gitlab_http_status(302)
+ expect(response.headers).to include('Location')
+ end
+ end
end
end
context 'when using runnners token' do
let(:token) { job.project.runners_token }
+ before do
+ download_artifact
+ end
+
it 'responds with forbidden' do
expect(response).to have_gitlab_http_status(403)
end
@@ -1208,12 +1250,16 @@ describe API::Runner do
context 'when job does not has artifacts' do
it 'responds with not found' do
+ download_artifact
+
expect(response).to have_gitlab_http_status(404)
end
end
def download_artifact(params = {}, request_headers = headers)
params = params.merge(token: token)
+ job.reload
+
get api("/jobs/#{job.id}/artifacts"), params, request_headers
end
end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 9052a18c60b..f8d5258a8d9 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -99,10 +99,10 @@ describe API::Search do
end
end
- describe "GET /groups/:id/-/search" do
+ describe "GET /groups/:id/search" do
context 'when user is not authenticated' do
it 'returns 401 error' do
- get api("/groups/#{group.id}/-/search"), scope: 'projects', search: 'awesome'
+ get api("/groups/#{group.id}/search"), scope: 'projects', search: 'awesome'
expect(response).to have_gitlab_http_status(401)
end
@@ -110,7 +110,7 @@ describe API::Search do
context 'when scope is not supported' do
it 'returns 400 error' do
- get api("/groups/#{group.id}/-/search", user), scope: 'unsupported', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'unsupported', search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -118,7 +118,7 @@ describe API::Search do
context 'when scope is missing' do
it 'returns 400 error' do
- get api("/groups/#{group.id}/-/search", user), search: 'awesome'
+ get api("/groups/#{group.id}/search", user), search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -126,7 +126,7 @@ describe API::Search do
context 'when group does not exist' do
it 'returns 404 error' do
- get api('/groups/9999/-/search', user), scope: 'issues', search: 'awesome'
+ get api('/groups/9999/search', user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -136,7 +136,7 @@ describe API::Search do
it 'returns 404 error' do
private_group = create(:group, :private)
- get api("/groups/#{private_group.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/groups/#{private_group.id}/search", user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -145,7 +145,7 @@ describe API::Search do
context 'with correct params' do
context 'for projects scope' do
before do
- get api("/groups/#{group.id}/-/search", user), scope: 'projects', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'projects', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/projects'
@@ -155,7 +155,7 @@ describe API::Search do
before do
create(:issue, project: project, title: 'awesome issue')
- get api("/groups/#{group.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'issues', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
@@ -165,7 +165,7 @@ describe API::Search do
before do
create(:merge_request, source_project: repo_project, title: 'awesome mr')
- get api("/groups/#{group.id}/-/search", user), scope: 'merge_requests', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'merge_requests', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
@@ -175,7 +175,7 @@ describe API::Search do
before do
create(:milestone, project: project, title: 'awesome milestone')
- get api("/groups/#{group.id}/-/search", user), scope: 'milestones', search: 'awesome'
+ get api("/groups/#{group.id}/search", user), scope: 'milestones', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
@@ -187,7 +187,7 @@ describe API::Search do
create(:milestone, project: project, title: 'awesome milestone')
create(:milestone, project: another_project, title: 'awesome milestone other project')
- get api("/groups/#{CGI.escape(group.full_path)}/-/search", user), scope: 'milestones', search: 'awesome'
+ get api("/groups/#{CGI.escape(group.full_path)}/search", user), scope: 'milestones', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
@@ -198,7 +198,7 @@ describe API::Search do
describe "GET /projects/:id/search" do
context 'when user is not authenticated' do
it 'returns 401 error' do
- get api("/projects/#{project.id}/-/search"), scope: 'issues', search: 'awesome'
+ get api("/projects/#{project.id}/search"), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(401)
end
@@ -206,7 +206,7 @@ describe API::Search do
context 'when scope is not supported' do
it 'returns 400 error' do
- get api("/projects/#{project.id}/-/search", user), scope: 'unsupported', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'unsupported', search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -214,7 +214,7 @@ describe API::Search do
context 'when scope is missing' do
it 'returns 400 error' do
- get api("/projects/#{project.id}/-/search", user), search: 'awesome'
+ get api("/projects/#{project.id}/search", user), search: 'awesome'
expect(response).to have_gitlab_http_status(400)
end
@@ -222,7 +222,7 @@ describe API::Search do
context 'when project does not exist' do
it 'returns 404 error' do
- get api('/projects/9999/-/search', user), scope: 'issues', search: 'awesome'
+ get api('/projects/9999/search', user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -232,7 +232,7 @@ describe API::Search do
it 'returns 404 error' do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- get api("/projects/#{project.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'issues', search: 'awesome'
expect(response).to have_gitlab_http_status(404)
end
@@ -243,7 +243,7 @@ describe API::Search do
before do
create(:issue, project: project, title: 'awesome issue')
- get api("/projects/#{project.id}/-/search", user), scope: 'issues', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'issues', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
@@ -253,7 +253,7 @@ describe API::Search do
before do
create(:merge_request, source_project: repo_project, title: 'awesome mr')
- get api("/projects/#{repo_project.id}/-/search", user), scope: 'merge_requests', search: 'awesome'
+ get api("/projects/#{repo_project.id}/search", user), scope: 'merge_requests', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
@@ -263,7 +263,7 @@ describe API::Search do
before do
create(:milestone, project: project, title: 'awesome milestone')
- get api("/projects/#{project.id}/-/search", user), scope: 'milestones', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'milestones', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
@@ -273,7 +273,7 @@ describe API::Search do
before do
create(:note_on_merge_request, project: project, note: 'awesome note')
- get api("/projects/#{project.id}/-/search", user), scope: 'notes', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'notes', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/notes'
@@ -284,7 +284,7 @@ describe API::Search do
wiki = create(:project_wiki, project: project)
create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: "Awesome page" })
- get api("/projects/#{project.id}/-/search", user), scope: 'wiki_blobs', search: 'awesome'
+ get api("/projects/#{project.id}/search", user), scope: 'wiki_blobs', search: 'awesome'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs'
@@ -292,7 +292,7 @@ describe API::Search do
context 'for commits scope' do
before do
- get api("/projects/#{repo_project.id}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
+ get api("/projects/#{repo_project.id}/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details'
@@ -300,7 +300,7 @@ describe API::Search do
context 'for commits scope with project path as id' do
before do
- get api("/projects/#{CGI.escape(repo_project.full_path)}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
+ get api("/projects/#{CGI.escape(repo_project.full_path)}/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details'
@@ -308,7 +308,7 @@ describe API::Search do
context 'for blobs scope' do
before do
- get api("/projects/#{repo_project.id}/-/search", user), scope: 'blobs', search: 'monitors'
+ get api("/projects/#{repo_project.id}/search", user), scope: 'blobs', search: 'monitors'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs', size: 2
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index de1619f33c1..6bb53fdc98d 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -65,7 +65,7 @@ describe API::Templates do
expect(json_response['description']).to include('A short and simple permissive license with conditions')
expect(json_response['conditions']).to eq(%w[include-copyright])
expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
- expect(json_response['limitations']).to eq(%w[no-liability])
+ expect(json_response['limitations']).to eq(%w[liability warranty])
expect(json_response['content']).to include('MIT License')
end
end
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index 79041c6a792..00f067889a0 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -216,6 +216,7 @@ describe API::V3::Builds do
describe 'GET /projects/:id/builds/:build_id/artifacts' do
before do
+ stub_artifacts_object_storage
get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
end
@@ -230,13 +231,24 @@ describe API::V3::Builds do
end
it 'returns specific job artifacts' do
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to have_http_status(200)
expect(response.headers).to include(download_headers)
expect(response.body).to match_file(build.artifacts_file.file.file)
end
end
end
+ context 'when artifacts are stored remotely' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: build) }
+
+ it 'returns location redirect' do
+ get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
context 'unauthorized user' do
let(:api_user) { nil }
@@ -256,6 +268,7 @@ describe API::V3::Builds do
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
before do
+ stub_artifacts_object_storage
build.success
end
@@ -318,9 +331,24 @@ describe API::V3::Builds do
"attachment; filename=#{build.artifacts_file.filename}" }
end
- it { expect(response).to have_gitlab_http_status(200) }
+ it { expect(response).to have_http_status(200) }
it { expect(response.headers).to include(download_headers) }
end
+
+ context 'when artifacts are stored remotely' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:artifact) { create(:ci_job_artifact, :archive, :remote_store, job: build) }
+
+ before do
+ build.reload
+
+ get v3_api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
+ end
+
+ it 'returns location redirect' do
+ expect(response).to have_http_status(302)
+ end
+ end
end
context 'with regular branch' do
diff --git a/spec/requests/api/v3/templates_spec.rb b/spec/requests/api/v3/templates_spec.rb
index 38a8994eb79..1a637f3cf96 100644
--- a/spec/requests/api/v3/templates_spec.rb
+++ b/spec/requests/api/v3/templates_spec.rb
@@ -57,7 +57,7 @@ describe API::V3::Templates do
expect(json_response['description']).to include('A short and simple permissive license with conditions')
expect(json_response['conditions']).to eq(%w[include-copyright])
expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use])
- expect(json_response['limitations']).to eq(%w[no-liability])
+ expect(json_response['limitations']).to eq(%w[liability warranty])
expect(json_response['content']).to include('MIT License')
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 6dbbb1ad7bb..494db30e8e0 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -163,7 +163,7 @@ describe 'Git HTTP requests' do
download(path) do |response|
json_body = ActiveSupport::JSON.decode(response.body)
- expect(json_body['RepoPath']).to include(wiki.repository.disk_path)
+ expect(json_body['Repository']['relative_path']).to eq(wiki.repository.relative_path)
end
end
end
@@ -344,20 +344,11 @@ describe 'Git HTTP requests' do
context 'and the user requests a redirected path' do
let!(:redirect) { project.route.create_redirect('foo/bar') }
let(:path) { "#{redirect.path}.git" }
- let(:project_moved_message) do
- <<-MSG.strip_heredoc
- Project '#{redirect.path}' was moved to '#{project.full_path}'.
- Please update your Git remote:
-
- git remote set-url origin #{project.http_url_to_repo} and try again.
- MSG
- end
-
- it 'downloads get status 404 with "project was moved" message' do
+ it 'downloads get status 200 for redirects' do
clone_get(path, {})
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to match(project_moved_message)
+
+ expect(response).to have_gitlab_http_status(:ok)
end
end
end
@@ -559,20 +550,19 @@ describe 'Git HTTP requests' do
Please update your Git remote:
- git remote set-url origin #{project.http_url_to_repo} and try again.
+ git remote set-url origin #{project.http_url_to_repo}.
MSG
end
- it 'downloads get status 404 with "project was moved" message' do
+ it 'downloads get status 200' do
clone_get(path, env)
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to match(project_moved_message)
+
+ expect(response).to have_gitlab_http_status(:ok)
end
it 'uploads get status 404 with "project was moved" message' do
upload(path, env) do |response|
- expect(response).to have_gitlab_http_status(:not_found)
- expect(response.body).to match(project_moved_message)
+ expect(response).to have_gitlab_http_status(:ok)
end
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 971b45c411d..1e6bd993c08 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -191,10 +191,12 @@ describe 'Git LFS API and storage' do
describe 'when fetching lfs object' do
let(:project) { create(:project) }
let(:update_permissions) { }
+ let(:before_get) { }
before do
enable_lfs
update_permissions
+ before_get
get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
end
@@ -239,6 +241,38 @@ describe 'Git LFS API and storage' do
end
it_behaves_like 'responds with a file'
+
+ context 'when LFS uses object storage' do
+ context 'when proxy download is enabled' do
+ let(:before_get) do
+ stub_lfs_object_storage(proxy_download: true)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
+
+ it 'responds with redirect' do
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'responds with the workhorse send-url' do
+ expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
+ end
+ end
+
+ context 'when proxy download is disabled' do
+ let(:before_get) do
+ stub_lfs_object_storage(proxy_download: false)
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
+
+ it 'responds with redirect' do
+ expect(response).to have_gitlab_http_status(302)
+ end
+
+ it 'responds with the file location' do
+ expect(response.location).to include(lfs_object.reload.file.path)
+ end
+ end
+ end
end
end
@@ -945,22 +979,61 @@ describe 'Git LFS API and storage' do
end
context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- put_authorize
+ shared_examples 'a valid response' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'uses the gitlab-workhorse content type' do
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
end
- it 'responds with status 200' do
- expect(response).to have_gitlab_http_status(200)
+ shared_examples 'a local file' do
+ it_behaves_like 'a valid response' do
+ it 'responds with status 200, location of lfs store and object details' do
+ expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
+ expect(json_response['RemoteObject']).to be_nil
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
end
- it 'uses the gitlab-workhorse content type' do
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ context 'when using local storage' do
+ it_behaves_like 'a local file'
end
- it 'responds with status 200, location of lfs store and object details' do
- expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path)
- expect(json_response['LfsOid']).to eq(sample_oid)
- expect(json_response['LfsSize']).to eq(sample_size)
+ context 'when using remote storage' do
+ context 'when direct upload is enabled' do
+ before do
+ stub_lfs_object_storage(enabled: true, direct_upload: true)
+ end
+
+ it_behaves_like 'a valid response' do
+ it 'responds with status 200, location of lfs remote store and object details' do
+ expect(json_response['TempPath']).to be_nil
+ expect(json_response['RemoteObject']).to have_key('ID')
+ expect(json_response['RemoteObject']).to have_key('GetURL')
+ expect(json_response['RemoteObject']).to have_key('StoreURL')
+ expect(json_response['RemoteObject']).to have_key('DeleteURL')
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ stub_lfs_object_storage(enabled: true, direct_upload: false)
+ end
+
+ it_behaves_like 'a local file'
+ end
end
end
@@ -978,14 +1051,95 @@ describe 'Git LFS API and storage' do
end
end
+ context 'and workhorse requests upload finalize for a new lfs object' do
+ before do
+ lfs_object.destroy
+ end
+
+ context 'with object storage disabled' do
+ it "doesn't attempt to migrate file to object storage" do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ put_finalize(with_tempfile: true)
+ end
+ end
+
+ context 'with object storage enabled' do
+ context 'and direct upload enabled' do
+ let!(:fog_connection) do
+ stub_lfs_object_storage(direct_upload: true)
+ end
+
+ ['123123', '../../123123'].each do |remote_id|
+ context "with invalid remote_id: #{remote_id}" do
+ subject do
+ put_finalize_with_args('file.remote_id' => remote_id)
+ end
+
+ it 'responds with status 403' do
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'with valid remote_id' do
+ before do
+ fog_connection.directories.get('lfs-objects').files.create(
+ key: 'tmp/upload/12312300',
+ body: 'content'
+ )
+ end
+
+ subject do
+ put_finalize_with_args(
+ 'file.remote_id' => '12312300',
+ 'file.name' => 'name')
+ end
+
+ it 'responds with status 200' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'schedules migration of file to object storage' do
+ subject
+
+ expect(LfsObject.last.projects).to include(project)
+ end
+
+ it 'have valid file' do
+ subject
+
+ expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(LfsObject.last.file).to be_exists
+ end
+ end
+ end
+
+ context 'and background upload enabled' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
+
+ it 'schedules migration of file to object storage' do
+ expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric))
+
+ put_finalize(with_tempfile: true)
+ end
+ end
+ end
+ end
+
context 'invalid tempfiles' do
- it 'rejects slashes in the tempfile name (path traversal' do
- put_finalize('foo/bar')
- expect(response).to have_gitlab_http_status(403)
+ before do
+ lfs_object.destroy
end
- it 'rejects tempfile names that do not start with the oid' do
- put_finalize("foo#{sample_oid}")
+ it 'rejects slashes in the tempfile name (path traversal)' do
+ put_finalize('../bar', with_tempfile: true)
expect(response).to have_gitlab_http_status(403)
end
end
@@ -1075,7 +1229,7 @@ describe 'Git LFS API and storage' do
end
it 'with location of lfs store and object details' do
- expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path)
+ expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end
@@ -1177,9 +1331,25 @@ describe 'Git LFS API and storage' do
put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, authorize_headers
end
- def put_finalize(lfs_tmp = lfs_tmp_file)
- put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", nil,
- headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp).compact
+ def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false)
+ upload_path = LfsObjectUploader.workhorse_local_upload_path
+ file_path = upload_path + '/' + lfs_tmp if lfs_tmp
+
+ if with_tempfile
+ FileUtils.mkdir_p(upload_path)
+ FileUtils.touch(file_path)
+ end
+
+ args = {
+ 'file.path' => file_path,
+ 'file.name' => File.basename(file_path)
+ }.compact
+
+ put_finalize_with_args(args)
+ end
+
+ def put_finalize_with_args(args)
+ put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", args, headers
end
def lfs_tmp_file
diff --git a/spec/rubocop/cop/gitlab/httparty_spec.rb b/spec/rubocop/cop/gitlab/httparty_spec.rb
new file mode 100644
index 00000000000..510839a21d7
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/httparty_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/httparty'
+
+describe RuboCop::Cop::Gitlab::HTTParty do # rubocop:disable RSpec/FilePath
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ shared_examples('registering include offense') do |options|
+ let(:offending_lines) { options[:offending_lines] }
+
+ it 'registers an offense when the class includes HTTParty' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(offending_lines.size)
+ expect(cop.offenses.map(&:line)).to eq(offending_lines)
+ end
+ end
+ end
+
+ shared_examples('registering call offense') do |options|
+ let(:offending_lines) { options[:offending_lines] }
+
+ it 'registers an offense when the class calls HTTParty' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(offending_lines.size)
+ expect(cop.offenses.map(&:line)).to eq(offending_lines)
+ end
+ end
+ end
+
+ context 'when source is a regular module' do
+ it_behaves_like 'registering include offense', offending_lines: [2] do
+ let(:source) do
+ <<~RUBY
+ module M
+ include HTTParty
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when source is a regular class' do
+ it_behaves_like 'registering include offense', offending_lines: [2] do
+ let(:source) do
+ <<~RUBY
+ class Foo
+ include HTTParty
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'when HTTParty is called' do
+ it_behaves_like 'registering call offense', offending_lines: [3] do
+ let(:source) do
+ <<~RUBY
+ class Foo
+ def bar
+ HTTParty.get('http://example.com')
+ end
+ end
+ RUBY
+ end
+ end
+ end
+end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index c38795ad1a1..f51c11b141f 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -117,6 +117,7 @@ describe PipelineSerializer do
shared_examples 'no N+1 queries' do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
+
expect(recorded.count).to be_within(1).of(36)
expect(recorded.cached_count).to eq(0)
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 0ce41e7c7ee..feb5120bc68 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -9,6 +9,8 @@ describe Ci::ProcessPipelineService, '#execute' do
end
before do
+ stub_ci_pipeline_to_return_yaml_file
+
stub_not_protect_default_branch
project.add_developer(user)
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index db9c216d3f4..b86a3d72bb4 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -28,7 +28,8 @@ describe Ci::RetryBuildService do
%i[type lock_version target_url base_tags trace_sections
commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id
- user_id auto_canceled_by_id retried failure_reason].freeze
+ user_id auto_canceled_by_id retried failure_reason
+ artifacts_file_store artifacts_metadata_store].freeze
shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index e2e64659dfa..1c2f9c5cf43 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -82,7 +82,7 @@ describe Clusters::CreateService do
context 'when project has a cluster' do
include_context 'valid params'
- let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, projects: [project]) }
it 'does not create a cluster' do
expect(ClusterProvisionWorker).not_to receive(:perform_async)
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index e1c873f8c1e..999677cfaaa 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -222,8 +222,8 @@ describe Groups::TransferService, :postgresql do
expect(new_parent_group.children.first).to eq(group)
end
- it 'should create a permanent redirect for the group' do
- expect(group.redirect_routes.permanent.count).to eq(1)
+ it 'should create a redirect for the group' do
+ expect(group.redirect_routes.count).to eq(1)
end
end
@@ -243,10 +243,10 @@ describe Groups::TransferService, :postgresql do
end
end
- it 'should create permanent redirects for the subgroups' do
- expect(group.redirect_routes.permanent.count).to eq(1)
- expect(subgroup1.redirect_routes.permanent.count).to eq(1)
- expect(subgroup2.redirect_routes.permanent.count).to eq(1)
+ it 'should create redirects for the subgroups' do
+ expect(group.redirect_routes.count).to eq(1)
+ expect(subgroup1.redirect_routes.count).to eq(1)
+ expect(subgroup2.redirect_routes.count).to eq(1)
end
context 'when the new parent has a higher visibility than the children' do
@@ -287,9 +287,9 @@ describe Groups::TransferService, :postgresql do
end
it 'should create permanent redirects for the projects' do
- expect(group.redirect_routes.permanent.count).to eq(1)
- expect(project1.redirect_routes.permanent.count).to eq(1)
- expect(project2.redirect_routes.permanent.count).to eq(1)
+ expect(group.redirect_routes.count).to eq(1)
+ expect(project1.redirect_routes.count).to eq(1)
+ expect(project2.redirect_routes.count).to eq(1)
end
context 'when the new parent has a higher visibility than the projects' do
@@ -338,12 +338,12 @@ describe Groups::TransferService, :postgresql do
end
end
- it 'should create permanent redirect for the subgroups and projects' do
- expect(group.redirect_routes.permanent.count).to eq(1)
- expect(subgroup1.redirect_routes.permanent.count).to eq(1)
- expect(subgroup2.redirect_routes.permanent.count).to eq(1)
- expect(project1.redirect_routes.permanent.count).to eq(1)
- expect(project2.redirect_routes.permanent.count).to eq(1)
+ it 'should create redirect for the subgroups and projects' do
+ expect(group.redirect_routes.count).to eq(1)
+ expect(subgroup1.redirect_routes.count).to eq(1)
+ expect(subgroup2.redirect_routes.count).to eq(1)
+ expect(project1.redirect_routes.count).to eq(1)
+ expect(project2.redirect_routes.count).to eq(1)
end
end
@@ -380,12 +380,12 @@ describe Groups::TransferService, :postgresql do
end
end
- it 'should create permanent redirect for the subgroups and projects' do
- expect(group.redirect_routes.permanent.count).to eq(1)
- expect(project1.redirect_routes.permanent.count).to eq(1)
- expect(subgroup1.redirect_routes.permanent.count).to eq(1)
- expect(nested_subgroup.redirect_routes.permanent.count).to eq(1)
- expect(nested_project.redirect_routes.permanent.count).to eq(1)
+ it 'should create redirect for the subgroups and projects' do
+ expect(group.redirect_routes.count).to eq(1)
+ expect(project1.redirect_routes.count).to eq(1)
+ expect(subgroup1.redirect_routes.count).to eq(1)
+ expect(nested_subgroup.redirect_routes.count).to eq(1)
+ expect(nested_project.redirect_routes.count).to eq(1)
end
end
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index c148a98569b..a9aee9e100f 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -6,7 +6,7 @@ describe Issues::MoveService do
let(:title) { 'Some issue' }
let(:description) { 'Some issue description' }
let(:old_project) { create(:project) }
- let(:new_project) { create(:project, group: create(:group)) }
+ let(:new_project) { create(:project) }
let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') }
let(:old_issue) do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 903aa0a5078..2536c6e2514 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -24,6 +24,14 @@ describe MergeRequests::RefreshService do
merge_when_pipeline_succeeds: true,
merge_user: @user)
+ @another_merge_request = create(:merge_request,
+ source_project: @project,
+ source_branch: 'master',
+ target_branch: 'test',
+ target_project: @project,
+ merge_when_pipeline_succeeds: true,
+ merge_user: @user)
+
@fork_merge_request = create(:merge_request,
source_project: @fork_project,
source_branch: 'master',
@@ -52,9 +60,11 @@ describe MergeRequests::RefreshService do
context 'push to origin repo source branch' do
let(:refresh_service) { service.new(@project, @user) }
+ let(:notification_service) { spy('notification_service') }
before do
allow(refresh_service).to receive(:execute_hooks)
+ allow(NotificationService).to receive(:new) { notification_service }
end
it 'executes hooks with update action' do
@@ -64,6 +74,11 @@ describe MergeRequests::RefreshService do
expect(refresh_service).to have_received(:execute_hooks)
.with(@merge_request, 'update', old_rev: @oldrev)
+ expect(notification_service).to have_received(:push_to_merge_request)
+ .with(@merge_request, @user, new_commits: anything, existing_commits: anything)
+ expect(notification_service).to have_received(:push_to_merge_request)
+ .with(@another_merge_request, @user, new_commits: anything, existing_commits: anything)
+
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
@@ -119,11 +134,13 @@ describe MergeRequests::RefreshService do
context 'push to origin repo source branch when an MR was reopened' do
let(:refresh_service) { service.new(@project, @user) }
+ let(:notification_service) { spy('notification_service') }
before do
@merge_request.update(state: :reopened)
allow(refresh_service).to receive(:execute_hooks)
+ allow(NotificationService).to receive(:new) { notification_service }
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
end
@@ -131,6 +148,10 @@ describe MergeRequests::RefreshService do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks)
.with(@merge_request, 'update', old_rev: @oldrev)
+ expect(notification_service).to have_received(:push_to_merge_request)
+ .with(@merge_request, @user, new_commits: anything, existing_commits: anything)
+ expect(notification_service).to have_received(:push_to_merge_request)
+ .with(@another_merge_request, @user, new_commits: anything, existing_commits: anything)
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 62fdf870090..f8fa2540804 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -34,6 +34,12 @@ describe NotificationService, :mailer do
should_not_email_anyone
end
+ it 'emails new mentions despite being unsubscribed' do
+ send_notifications(@unsubscribed_mentioned)
+
+ should_only_email(@unsubscribed_mentioned)
+ end
+
it 'sends the proper notification reason header' do
send_notifications(@u_watcher)
should_only_email(@u_watcher)
@@ -122,7 +128,7 @@ describe NotificationService, :mailer do
let(:project) { create(:project, :private) }
let(:issue) { create(:issue, project: project, assignees: [assignee]) }
let(:mentioned_issue) { create(:issue, assignees: issue.assignees) }
- let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @outsider also') }
+ let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @unsubscribed_mentioned and @outsider also') }
before do
build_team(note.project)
@@ -150,7 +156,7 @@ describe NotificationService, :mailer do
add_users_with_subscription(note.project, issue)
reset_delivered_emails!
- expect(SentNotification).to receive(:record).with(issue, any_args).exactly(9).times
+ expect(SentNotification).to receive(:record).with(issue, any_args).exactly(10).times
notification.new_note(note)
@@ -163,6 +169,7 @@ describe NotificationService, :mailer do
should_email(@watcher_and_subscriber)
should_email(@subscribed_participant)
should_email(@u_custom_off)
+ should_email(@unsubscribed_mentioned)
should_not_email(@u_guest_custom)
should_not_email(@u_guest_watcher)
should_not_email(note.author)
@@ -279,6 +286,7 @@ describe NotificationService, :mailer do
before do
build_team(note.project)
note.project.add_master(note.author)
+ add_users_with_subscription(note.project, issue)
reset_delivered_emails!
end
@@ -286,6 +294,9 @@ describe NotificationService, :mailer do
it 'notifies the team members' do
notification.new_note(note)
+ # Make sure @unsubscribed_mentioned is part of the team
+ expect(note.project.team.members).to include(@unsubscribed_mentioned)
+
# Notify all team members
note.project.team.members.each do |member|
# User with disabled notification should not be notified
@@ -486,7 +497,7 @@ describe NotificationService, :mailer do
let(:group) { create(:group) }
let(:project) { create(:project, :public, namespace: group) }
let(:another_project) { create(:project, :public, namespace: group) }
- let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant' }
+ let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant @unsubscribed_mentioned' }
before do
build_team(issue.project)
@@ -510,6 +521,7 @@ describe NotificationService, :mailer do
should_email(@u_participant_mentioned)
should_email(@g_global_watcher)
should_email(@g_watcher)
+ should_email(@unsubscribed_mentioned)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -1078,6 +1090,36 @@ describe NotificationService, :mailer do
end
end
+ describe '#push_to_merge_request' do
+ before do
+ update_custom_notification(:push_to_merge_request, @u_guest_custom, resource: project)
+ update_custom_notification(:push_to_merge_request, @u_custom_global)
+ end
+
+ it do
+ notification.push_to_merge_request(merge_request, @u_disabled)
+
+ should_email(merge_request.assignee)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@u_watcher)
+ should_not_email(@u_guest_watcher)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { merge_request }
+ let(:notification_trigger) { notification.push_to_merge_request(merge_request, @u_disabled) }
+ end
+ end
+
describe '#relabel_merge_request' do
let(:group_label_1) { create(:group_label, group: group, title: 'Group Label 1', merge_requests: [merge_request]) }
let(:group_label_2) { create(:group_label, group: group, title: 'Group Label 2') }
@@ -1823,6 +1865,7 @@ describe NotificationService, :mailer do
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
+ @unsubscribed_mentioned = create :user, username: 'unsubscribed_mentioned'
@subscribed_participant = create_global_setting_for(create(:user, username: 'subscribed_participant'), :participating)
@watcher_and_subscriber = create_global_setting_for(create(:user), :watch)
@@ -1830,7 +1873,9 @@ describe NotificationService, :mailer do
project.add_master(@subscriber)
project.add_master(@unsubscriber)
project.add_master(@watcher_and_subscriber)
+ project.add_master(@unsubscribed_mentioned)
+ issuable.subscriptions.create(user: @unsubscribed_mentioned, project: project, subscribed: false)
issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true)
issuable.subscriptions.create(user: @subscribed_participant, project: project, subscribed: true)
issuable.subscriptions.create(user: @unsubscriber, project: project, subscribed: false)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 8471467d2fa..4413c6ef83e 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -153,7 +153,7 @@ describe Projects::CreateService, '#execute' do
context 'when another repository already exists on disk' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
let(:opts) do
{
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index d1011b07db6..0f7c46367d0 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -105,7 +105,7 @@ describe Projects::ForkService do
context 'repository already exists' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
before do
gitlab_shell.create_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index ce567fe3879..95a6771c59d 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -146,7 +146,7 @@ describe Projects::TransferService do
context 'namespace which contains orphan repository with same projects path name' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
before do
group.add_owner(user)
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index f3f97b6b921..f48d466d263 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -190,7 +190,7 @@ describe Projects::UpdateService do
context 'when renaming a project' do
let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path }
context 'with legacy storage' do
let(:project) { create(:project, :legacy_storage, :repository, creator: user, namespace: user.namespace) }
@@ -241,6 +241,27 @@ describe Projects::UpdateService do
})
end
end
+
+ context 'when updating #pages_https_only', :https_pages_enabled do
+ subject(:call_service) do
+ update_project(project, admin, pages_https_only: false)
+ end
+
+ it 'updates the attribute' do
+ expect { call_service }
+ .to change { project.pages_https_only? }
+ .to(false)
+ end
+
+ it 'calls Projects::UpdatePagesConfigurationService' do
+ expect(Projects::UpdatePagesConfigurationService)
+ .to receive(:new)
+ .with(project)
+ .and_call_original
+
+ call_service
+ end
+ end
end
describe '#run_auto_devops_pipeline?' do
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 53b3e5e365d..786493c3577 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -35,5 +35,18 @@ describe ProtectedBranches::CreateService do
expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
+
+ context 'when a policy restricts rule creation' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents creation of the protected branch rule" do
+ expect do
+ service.execute
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
end
end
diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb
new file mode 100644
index 00000000000..4a391b6c25c
--- /dev/null
+++ b/spec/services/protected_branches/destroy_service_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe ProtectedBranches::DestroyService do
+ let(:protected_branch) { create(:protected_branch) }
+ let(:project) { protected_branch.project }
+ let(:user) { project.owner }
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user) }
+
+ it 'destroys a protected branch' do
+ service.execute(protected_branch)
+
+ expect(protected_branch).to be_destroyed
+ end
+
+ context 'when a policy restricts rule deletion' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents deletion of the protected branch rule" do
+ expect do
+ service.execute(protected_branch)
+ end.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+ end
+end
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index 9fa5983db66..3f6f8e09565 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -22,5 +22,16 @@ describe ProtectedBranches::UpdateService do
expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
+
+ context 'when a policy restricts rule creation' do
+ before do
+ policy = instance_double(ProtectedBranchPolicy, can?: false)
+ expect(ProtectedBranchPolicy).to receive(:new).and_return(policy)
+ end
+
+ it "prevents creation of the protected branch rule" do
+ expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
end
end
diff --git a/spec/services/protected_tags/destroy_service_spec.rb b/spec/services/protected_tags/destroy_service_spec.rb
new file mode 100644
index 00000000000..e12f53a2221
--- /dev/null
+++ b/spec/services/protected_tags/destroy_service_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe ProtectedTags::DestroyService do
+ let(:protected_tag) { create(:protected_tag) }
+ let(:project) { protected_tag.project }
+ let(:user) { project.owner }
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user) }
+
+ it 'destroy a protected tag' do
+ service.execute(protected_tag)
+
+ expect(protected_tag).to be_destroyed
+ end
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index a3893188c6e..e28b0ea5cf2 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -743,7 +743,7 @@ describe SystemNoteService do
expect(cross_reference(type)).to eq("Events for #{type.pluralize.humanize.downcase} are disabled.")
end
- it "blocks cross reference when #{type.underscore}_events is true" do
+ it "creates cross reference when #{type.underscore}_events is true" do
jira_tracker.update("#{type}_events" => true)
expect(cross_reference(type)).to eq(success_message)
diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb
index 576db1dde2d..d974cc0226f 100644
--- a/spec/services/verify_pages_domain_service_spec.rb
+++ b/spec/services/verify_pages_domain_service_spec.rb
@@ -93,6 +93,25 @@ describe VerifyPagesDomainService do
expect(domain).not_to be_enabled
end
end
+
+ context 'invalid domain' do
+ let(:domain) { build(:pages_domain, :expired, :with_missing_chain) }
+
+ before do
+ domain.save(validate: false)
+ end
+
+ it 'can be disabled' do
+ error_status[:message] += '. It is now disabled.'
+
+ stub_resolver
+
+ expect(service.execute).to eq(error_status)
+
+ expect(domain).not_to be_verified
+ expect(domain).not_to be_enabled
+ end
+ end
end
context 'timeout behaviour' do
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 21910e69d2e..2ef2e61babc 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -14,6 +14,20 @@ describe WebHookService do
end
let(:service_instance) { described_class.new(project_hook, data, :push_hooks) }
+ describe '#initialize' do
+ it 'allow_local_requests is true if hook is a SystemHook' do
+ instance = described_class.new(build(:system_hook), data, :system_hook)
+ expect(instance.request_options[:allow_local_requests]).to be_truthy
+ end
+
+ it 'allow_local_requests is false if hook is not a SystemHook' do
+ %i(project_hook service_hook web_hook_log).each do |hook|
+ instance = described_class.new(build(hook), data, hook)
+ expect(instance.request_options[:allow_local_requests]).to be_falsey
+ end
+ end
+ end
+
describe '#execute' do
before do
project.hooks << [project_hook]
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 9f6f0204a16..e8cecf361ff 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -97,6 +97,10 @@ RSpec.configure do |config|
TestEnv.init
end
+ config.after(:all) do
+ TestEnv.clean_test_path
+ end
+
config.before(:example) do
# Skip pre-receive hook check so we can use the web editor and merge.
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
@@ -197,6 +201,22 @@ RSpec.configure do |config|
Ability.allowed?(*args)
end
end
+
+ config.before(:each, :http_pages_enabled) do |_|
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
+ end
+
+ config.before(:each, :https_pages_enabled) do |_|
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443'])
+ end
+
+ config.before(:each, :http_pages_disabled) do |_|
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(false)
+ end
+
+ config.before(:each, :https_pages_disabled) do |_|
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(false)
+ end
end
# add simpler way to match asset paths containing digest strings
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
index c7e8a39a617..9cf541372b5 100644
--- a/spec/support/gitaly.rb
+++ b/spec/support/gitaly.rb
@@ -1,11 +1,13 @@
RSpec.configure do |config|
config.before(:each) do |example|
if example.metadata[:disable_gitaly]
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
+ # Use 'and_wrap_original' to make sure the arguments are valid
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) && false }
else
next if example.metadata[:skip_gitaly_mock]
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+ # Use 'and_wrap_original' to make sure the arguments are valid
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) || true }
end
end
end
diff --git a/spec/support/http_io/http_io_helpers.rb b/spec/support/http_io/http_io_helpers.rb
new file mode 100644
index 00000000000..31e07e720cd
--- /dev/null
+++ b/spec/support/http_io/http_io_helpers.rb
@@ -0,0 +1,64 @@
+module HttpIOHelpers
+ def stub_remote_trace_206
+ WebMock.stub_request(:get, remote_trace_url)
+ .to_return { |request| remote_trace_response(request, 206) }
+ end
+
+ def stub_remote_trace_200
+ WebMock.stub_request(:get, remote_trace_url)
+ .to_return { |request| remote_trace_response(request, 200) }
+ end
+
+ def stub_remote_trace_500
+ WebMock.stub_request(:get, remote_trace_url)
+ .to_return(status: [500, "Internal Server Error"])
+ end
+
+ def remote_trace_url
+ "http://trace.com/trace"
+ end
+
+ def remote_trace_response(request, responce_status)
+ range = request.headers['Range'].match(/bytes=(\d+)-(\d+)/)
+
+ {
+ status: responce_status,
+ headers: remote_trace_response_headers(responce_status, range[1].to_i, range[2].to_i),
+ body: range_trace_body(range[1].to_i, range[2].to_i)
+ }
+ end
+
+ def remote_trace_response_headers(responce_status, from, to)
+ headers = { 'Content-Type' => 'text/plain' }
+
+ if responce_status == 206
+ headers.merge('Content-Range' => "bytes #{from}-#{to}/#{remote_trace_size}")
+ end
+
+ headers
+ end
+
+ def range_trace_body(from, to)
+ remote_trace_body[from..to]
+ end
+
+ def remote_trace_body
+ @remote_trace_body ||= File.read(expand_fixture_path('trace/sample_trace'))
+ end
+
+ def remote_trace_size
+ remote_trace_body.length
+ end
+
+ def set_smaller_buffer_size_than(file_size)
+ blocks = (file_size / 128)
+ new_size = (blocks / 2) * 128
+ stub_const("Gitlab::Ci::Trace::HttpIO::BUFFER_SIZE", new_size)
+ end
+
+ def set_larger_buffer_size_than(file_size)
+ blocks = (file_size / 128)
+ new_size = (blocks * 2) * 128
+ stub_const("Gitlab::Ci::Trace::HttpIO::BUFFER_SIZE", new_size)
+ end
+end
diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb
index d7acf8c0032..b615a8f54cf 100644
--- a/spec/support/shared_examples/controllers/variables_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb
@@ -16,19 +16,19 @@ shared_examples 'PATCH #update updates variables' do
let(:variable_attributes) do
{ id: variable.id,
key: variable.key,
- value: variable.value,
+ secret_value: variable.value,
protected: variable.protected?.to_s }
end
let(:new_variable_attributes) do
{ key: 'new_key',
- value: 'dummy_value',
+ secret_value: 'dummy_value',
protected: 'false' }
end
context 'with invalid new variable parameters' do
let(:variables_attributes) do
[
- variable_attributes.merge(value: 'other_value'),
+ variable_attributes.merge(secret_value: 'other_value'),
new_variable_attributes.merge(key: '...?')
]
end
@@ -52,7 +52,7 @@ shared_examples 'PATCH #update updates variables' do
let(:variables_attributes) do
[
new_variable_attributes,
- new_variable_attributes.merge(value: 'other_value')
+ new_variable_attributes.merge(secret_value: 'other_value')
]
end
@@ -74,7 +74,7 @@ shared_examples 'PATCH #update updates variables' do
context 'with valid new variable parameters' do
let(:variables_attributes) do
[
- variable_attributes.merge(value: 'other_value'),
+ variable_attributes.merge(secret_value: 'other_value'),
new_variable_attributes
]
end
diff --git a/spec/support/shared_examples/models/atomic_internal_id_spec.rb b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
new file mode 100644
index 00000000000..144af4fc475
--- /dev/null
+++ b/spec/support/shared_examples/models/atomic_internal_id_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+shared_examples_for 'AtomicInternalId' do
+ describe '.has_internal_id' do
+ describe 'Module inclusion' do
+ subject { described_class }
+
+ it { is_expected.to include_module(AtomicInternalId) }
+ end
+
+ describe 'Validation' do
+ subject { instance }
+
+ before do
+ allow(InternalId).to receive(:generate_next).and_return(nil)
+ end
+
+ it { is_expected.to validate_presence_of(internal_id_attribute) }
+ it { is_expected.to validate_numericality_of(internal_id_attribute) }
+ end
+
+ describe 'internal id generation' do
+ subject { instance.save! }
+
+ it 'calls InternalId.generate_next and sets internal id attribute' do
+ iid = rand(1..1000)
+
+ expect(InternalId).to receive(:generate_next).with(instance, scope_attrs, usage, any_args).and_return(iid)
+ subject
+ expect(instance.public_send(internal_id_attribute)).to eq(iid)
+ end
+
+ it 'does not overwrite an existing internal id' do
+ instance.public_send("#{internal_id_attribute}=", 4711)
+
+ expect { subject }.not_to change { instance.public_send(internal_id_attribute) }
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
new file mode 100644
index 00000000000..cd9974cd6e2
--- /dev/null
+++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
@@ -0,0 +1,126 @@
+shared_context 'with storage' do |store, **stub_params|
+ before do
+ subject.object_store = store
+ end
+end
+
+shared_examples "migrates" do |to_store:, from_store: nil|
+ let(:to) { to_store }
+ let(:from) { from_store || subject.object_store }
+
+ def migrate(to)
+ subject.migrate!(to)
+ end
+
+ def checksum
+ Digest::SHA256.hexdigest(subject.read)
+ end
+
+ before do
+ migrate(from)
+ end
+
+ it 'returns corresponding file type' do
+ expect(subject).to be_an(CarrierWave::Uploader::Base)
+ expect(subject).to be_a(ObjectStorage::Concern)
+
+ if from == described_class::Store::REMOTE
+ expect(subject.file).to be_a(CarrierWave::Storage::Fog::File)
+ elsif from == described_class::Store::LOCAL
+ expect(subject.file).to be_a(CarrierWave::SanitizedFile)
+ else
+ raise 'Unexpected file type'
+ end
+ end
+
+ it 'does nothing when migrating to the current store' do
+ expect { migrate(from) }.not_to change { subject.object_store }.from(from)
+ end
+
+ it 'migrate to the specified store' do
+ from_checksum = checksum
+
+ expect { migrate(to) }.to change { subject.object_store }.from(from).to(to)
+ expect(checksum).to eq(from_checksum)
+ end
+
+ it 'removes the original file after the migration' do
+ original_file = subject.file.path
+ migrate(to)
+
+ expect(File.exist?(original_file)).to be_falsey
+ end
+
+ it 'can access to the original file during migration' do
+ file = subject.file
+
+ allow(subject).to receive(:delete_migrated_file) { } # Remove as a callback of :migrate
+ allow(subject).to receive(:record_upload) { } # Remove as a callback of :store (:record_upload)
+
+ expect(file.exists?).to be_truthy
+ expect { migrate(to) }.not_to change { file.exists? }
+ end
+
+ context 'when migrate! is not oqqupied by another process' do
+ it 'executes migrate!' do
+ expect(subject).to receive(:object_store=).at_least(1)
+
+ migrate(to)
+ end
+ end
+
+ context 'when migrate! is occupied by another process' do
+ let(:exclusive_lease_key) { "object_storage_migrate:#{subject.model.class}:#{subject.model.id}" }
+
+ before do
+ @uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
+ end
+
+ it 'does not execute migrate!' do
+ expect(subject).not_to receive(:unsafe_migrate!)
+
+ expect { migrate(to) }.to raise_error('Already running')
+ end
+
+ after do
+ Gitlab::ExclusiveLease.cancel(exclusive_lease_key, @uuid)
+ end
+ end
+
+ context 'migration is unsuccessful' do
+ shared_examples "handles gracefully" do |error:|
+ it 'does not update the object_store' do
+ expect { migrate(to) }.to raise_error(error)
+ expect(subject.object_store).to eq(from)
+ end
+
+ it 'does not delete the original file' do
+ expect { migrate(to) }.to raise_error(error)
+ expect(subject.exists?).to be_truthy
+ end
+ end
+
+ context 'when the store is not supported' do
+ let(:to) { -1 } # not a valid store
+
+ include_examples "handles gracefully", error: ObjectStorage::UnknownStoreError
+ end
+
+ context 'upon a fog failure' do
+ before do
+ storage_class = subject.send(:storage_for, to).class
+ expect_any_instance_of(storage_class).to receive(:store!).and_raise("Store failure.")
+ end
+
+ include_examples "handles gracefully", error: "Store failure."
+ end
+
+ context 'upon a database failure' do
+ before do
+ expect(uploader).to receive(:persist_object_store!).and_raise("ActiveRecord failure.")
+ end
+
+ include_examples "handles gracefully", error: "ActiveRecord failure."
+ end
+ end
+end
diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb
index 52e47ae2d34..21995c89a6e 100644
--- a/spec/support/stored_repositories.rb
+++ b/spec/support/stored_repositories.rb
@@ -4,7 +4,7 @@ RSpec.configure do |config|
end
config.before(:all, :broken_storage) do
- FileUtils.rm_rf Gitlab.config.repositories.storages.broken['path']
+ FileUtils.rm_rf Gitlab.config.repositories.storages.broken.legacy_disk_path
end
config.before(:each, :broken_storage) do
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 9f08c139322..bad1d34df3a 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -50,8 +50,12 @@ module StubConfiguration
# Default storage is always required
messages['default'] ||= Gitlab.config.repositories.storages.default
- messages.each do |storage_name, storage_settings|
- storage_settings['path'] = TestEnv.repos_path unless storage_settings.key?('path')
+ messages.each do |storage_name, storage_hash|
+ if !storage_hash.key?('path') || storage_hash['path'] == Gitlab::GitalyClient::StorageSettings::Deprecated
+ storage_hash['path'] = TestEnv.repos_path
+ end
+
+ messages[storage_name] = Gitlab::GitalyClient::StorageSettings.new(storage_hash.to_h)
end
allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages))
diff --git a/spec/support/stub_object_storage.rb b/spec/support/stub_object_storage.rb
new file mode 100644
index 00000000000..6e88641da42
--- /dev/null
+++ b/spec/support/stub_object_storage.rb
@@ -0,0 +1,48 @@
+module StubConfiguration
+ def stub_object_storage_uploader(
+ config:,
+ uploader:,
+ remote_directory:,
+ enabled: true,
+ proxy_download: false,
+ background_upload: false,
+ direct_upload: false
+ )
+ allow(config).to receive(:enabled) { enabled }
+ allow(config).to receive(:proxy_download) { proxy_download }
+ allow(config).to receive(:background_upload) { background_upload }
+ allow(config).to receive(:direct_upload) { direct_upload }
+
+ return unless enabled
+
+ Fog.mock!
+
+ ::Fog::Storage.new(uploader.object_store_credentials).tap do |connection|
+ begin
+ connection.directories.create(key: remote_directory)
+ rescue Excon::Error::Conflict
+ end
+ end
+ end
+
+ def stub_artifacts_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.artifacts.object_store,
+ uploader: JobArtifactUploader,
+ remote_directory: 'artifacts',
+ **params)
+ end
+
+ def stub_lfs_object_storage(**params)
+ stub_object_storage_uploader(config: Gitlab.config.lfs.object_store,
+ uploader: LfsObjectUploader,
+ remote_directory: 'lfs-objects',
+ **params)
+ end
+
+ def stub_uploads_object_storage(uploader = described_class, **params)
+ stub_object_storage_uploader(config: Gitlab.config.uploads.object_store,
+ uploader: uploader,
+ remote_directory: 'uploads',
+ **params)
+ end
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 01321989f01..f14e69b1041 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -225,7 +225,7 @@ module TestEnv
end
def repos_path
- Gitlab.config.repositories.storages.default['path']
+ Gitlab.config.repositories.storages.default.legacy_disk_path
end
def backup_path
diff --git a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
new file mode 100644
index 00000000000..8544fb62b5a
--- /dev/null
+++ b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
@@ -0,0 +1,118 @@
+require 'rake_helper'
+
+describe 'gitlab:artifacts namespace rake task' do
+ before(:context) do
+ Rake.application.rake_require 'tasks/gitlab/artifacts/migrate'
+ end
+
+ let(:object_storage_enabled) { false }
+
+ before do
+ stub_artifacts_object_storage(enabled: object_storage_enabled)
+ end
+
+ subject { run_rake_task('gitlab:artifacts:migrate') }
+
+ context 'legacy artifacts' do
+ describe 'migrate' do
+ let!(:build) { create(:ci_build, :legacy_artifacts, artifacts_file_store: store, artifacts_metadata_store: store) }
+
+ context 'when local storage is used' do
+ let(:store) { ObjectStorage::Store::LOCAL }
+
+ context 'and job does not have file store defined' do
+ let(:object_storage_enabled) { true }
+ let(:store) { nil }
+
+ it "migrates file to remote storage" do
+ subject
+
+ expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is defined' do
+ let(:object_storage_enabled) { true }
+
+ it "migrates file to remote storage" do
+ subject
+
+ expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is not defined' do
+ it "fails to migrate to remote storage" do
+ subject
+
+ expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::LOCAL)
+ expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
+
+ context 'when remote storage is used' do
+ let(:object_storage_enabled) { true }
+
+ let(:store) { ObjectStorage::Store::REMOTE }
+
+ it "file stays on remote storage" do
+ subject
+
+ expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
+ expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+ end
+
+ context 'job artifacts' do
+ let!(:artifact) { create(:ci_job_artifact, :archive, file_store: store) }
+
+ context 'when local storage is used' do
+ let(:store) { ObjectStorage::Store::LOCAL }
+
+ context 'and job does not have file store defined' do
+ let(:object_storage_enabled) { true }
+ let(:store) { nil }
+
+ it "migrates file to remote storage" do
+ subject
+
+ expect(artifact.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is defined' do
+ let(:object_storage_enabled) { true }
+
+ it "migrates file to remote storage" do
+ subject
+
+ expect(artifact.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is not defined' do
+ it "fails to migrate to remote storage" do
+ subject
+
+ expect(artifact.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
+
+ context 'when remote storage is used' do
+ let(:object_storage_enabled) { true }
+ let(:store) { ObjectStorage::Store::REMOTE }
+
+ it "file stays on remote storage" do
+ subject
+
+ expect(artifact.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 168facd51a6..0d24782f317 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -195,14 +195,23 @@ describe 'gitlab:app namespace rake task' do
end
context 'multiple repository storages' do
- let(:gitaly_address) { Gitlab.config.repositories.storages.default.gitaly_address }
+ let(:storage_default) do
+ Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage'))
+ end
+ let(:test_second_storage) do
+ Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/custom_storage'))
+ end
let(:storages) do
{
- 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address },
- 'test_second_storage' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
+ 'default' => storage_default,
+ 'test_second_storage' => test_second_storage
}
end
+ before(:all) do
+ @default_storage_hash = Gitlab.config.repositories.storages.default.to_h
+ end
+
before do
# We only need a backup of the repositories for this test
stub_env('SKIP', 'db,uploads,builds,artifacts,lfs,registry')
diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb
index 9e746ceddd6..2bf873c923f 100644
--- a/spec/tasks/gitlab/cleanup_rake_spec.rb
+++ b/spec/tasks/gitlab/cleanup_rake_spec.rb
@@ -6,13 +6,16 @@ describe 'gitlab:cleanup rake tasks' do
end
describe 'cleanup' do
- let(:gitaly_address) { Gitlab.config.repositories.storages.default.gitaly_address }
let(:storages) do
{
- 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address }
+ 'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage'))
}
end
+ before(:all) do
+ @default_storage_hash = Gitlab.config.repositories.storages.default.to_h
+ end
+
before do
FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage'))
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb
index 9aebf7b0b4a..1efaecc63a5 100644
--- a/spec/tasks/gitlab/git_rake_spec.rb
+++ b/spec/tasks/gitlab/git_rake_spec.rb
@@ -1,10 +1,13 @@
require 'rake_helper'
describe 'gitlab:git rake tasks' do
+ before(:all) do
+ @default_storage_hash = Gitlab.config.repositories.storages.default.to_h
+ end
+
before do
Rake.application.rake_require 'tasks/gitlab/git'
-
- storages = { 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') } }
+ storages = { 'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage')) }
FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/1/2/test.git'))
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 1f4053ff9ad..1e507c0236e 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -99,14 +99,14 @@ describe 'gitlab:gitaly namespace rake task' do
describe 'storage_config' do
it 'prints storage configuration in a TOML format' do
config = {
- 'default' => {
+ 'default' => Gitlab::GitalyClient::StorageSettings.new(
'path' => '/path/to/default',
'gitaly_address' => 'unix:/path/to/my.socket'
- },
- 'nfs_01' => {
+ ),
+ 'nfs_01' => Gitlab::GitalyClient::StorageSettings.new(
'path' => '/path/to/nfs_01',
'gitaly_address' => 'unix:/path/to/my.socket'
- }
+ )
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(config)
allow(Rails.env).to receive(:test?).and_return(false)
@@ -134,7 +134,7 @@ describe 'gitlab:gitaly namespace rake task' do
parsed_output = TomlRB.parse(expected_output)
config.each do |name, params|
- expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params['path'] })
+ expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params.legacy_disk_path })
end
end
end
diff --git a/spec/tasks/gitlab/lfs/migrate_rake_spec.rb b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
new file mode 100644
index 00000000000..66d1a192a96
--- /dev/null
+++ b/spec/tasks/gitlab/lfs/migrate_rake_spec.rb
@@ -0,0 +1,37 @@
+require 'rake_helper'
+
+describe 'gitlab:lfs namespace rake task' do
+ before :all do
+ Rake.application.rake_require 'tasks/gitlab/lfs/migrate'
+ end
+
+ describe 'migrate' do
+ let(:local) { ObjectStorage::Store::LOCAL }
+ let(:remote) { ObjectStorage::Store::REMOTE }
+ let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) }
+
+ def lfs_migrate
+ run_rake_task('gitlab:lfs:migrate')
+ end
+
+ context 'object storage disabled' do
+ before do
+ stub_lfs_object_storage(enabled: false)
+ end
+
+ it "doesn't migrate files" do
+ expect { lfs_migrate }.not_to change { lfs_object.reload.file_store }
+ end
+ end
+
+ context 'object storage enabled' do
+ before do
+ stub_lfs_object_storage
+ end
+
+ it 'migrates local file to object storage' do
+ expect { lfs_migrate }.to change { lfs_object.reload.file_store }.from(local).to(remote)
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index 65155cb044d..4a756c5742d 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -11,7 +11,7 @@ describe 'gitlab:shell rake tasks' do
it 'invokes create_hooks task' do
expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
- storages = Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
+ storages = Gitlab.config.repositories.storages.values.map(&:legacy_disk_path)
expect(Kernel).to receive(:system).with('bin/install', *storages).and_call_original
expect(Kernel).to receive(:system).with('bin/compile').and_call_original
diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
new file mode 100644
index 00000000000..b778d26060d
--- /dev/null
+++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
@@ -0,0 +1,28 @@
+require 'rake_helper'
+
+describe 'gitlab:uploads:migrate rake tasks' do
+ let!(:projects) { create_list(:project, 10, :with_avatar) }
+ let(:model_class) { Project }
+ let(:uploader_class) { AvatarUploader }
+ let(:mounted_as) { :avatar }
+ let(:batch_size) { 3 }
+
+ before do
+ stub_env('BATCH', batch_size.to_s)
+ stub_uploads_object_storage(uploader_class)
+ Rake.application.rake_require 'tasks/gitlab/uploads/migrate'
+
+ allow(ObjectStorage::MigrateUploadsWorker).to receive(:perform_async)
+ end
+
+ def run
+ args = [uploader_class.to_s, model_class.to_s, mounted_as].compact
+ run_rake_task("gitlab:uploads:migrate", *args)
+ end
+
+ it 'enqueue jobs in batch' do
+ expect(ObjectStorage::MigrateUploadsWorker).to receive(:enqueue!).exactly(4).times
+
+ run
+ end
+end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index 091ba824fc6..d302c14efb9 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -11,4 +11,26 @@ describe AttachmentUploader do
store_dir: %r[uploads/-/system/note/attachment/],
upload_path: %r[uploads/-/system/note/attachment/],
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/note/attachment/]
+
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[note/attachment/],
+ upload_path: %r[note/attachment/]
+ end
+
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
+ stub_uploads_object_storage
+ end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index bf9028c9260..b0468bc35ff 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe AvatarUploader do
- let(:model) { create(:user, :with_avatar) }
+ let(:model) { build_stubbed(:user) }
let(:uploader) { described_class.new(model, :avatar) }
let(:upload) { create(:upload, model: model) }
@@ -12,15 +12,28 @@ describe AvatarUploader do
upload_path: %r[uploads/-/system/user/avatar/],
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/avatar/]
- describe '#move_to_cache' do
- it 'is false' do
- expect(uploader.move_to_cache).to eq(false)
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[user/avatar/],
+ upload_path: %r[user/avatar/]
end
- describe '#move_to_store' do
- it 'is false' do
- expect(uploader.move_to_store).to eq(false)
+ context "with a file" do
+ let(:project) { create(:project, :with_avatar) }
+ let(:uploader) { project.avatar }
+ let(:upload) { uploader.upload }
+
+ before do
+ stub_uploads_object_storage
end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
end
end
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index bc024cd307c..68b7e24776d 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -36,6 +36,12 @@ describe FileMover do
it 'creates a new update record' do
expect { subject }.to change { Upload.count }.by(1)
end
+
+ it 'schedules a background migration' do
+ expect_any_instance_of(PersonalFileUploader).to receive(:schedule_background_upload).once
+
+ subject
+ end
end
context 'when update_markdown fails' do
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index b42ce982b27..db2810bbe1d 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -11,32 +11,41 @@ describe FileUploader do
shared_examples 'builds correct legacy storage paths' do
include_examples 'builds correct paths',
store_dir: %r{awesome/project/\h+},
+ upload_path: %r{\h+/<filename>},
absolute_path: %r{#{described_class.root}/awesome/project/secret/foo.jpg}
end
- shared_examples 'uses hashed storage' do
- context 'when rolled out attachments' do
- let(:project) { build_stubbed(:project, namespace: group, name: 'project') }
+ context 'legacy storage' do
+ it_behaves_like 'builds correct legacy storage paths'
- before do
- allow(project).to receive(:disk_path).and_return('ca/fe/fe/ed')
- end
+ context 'uses hashed storage' do
+ context 'when rolled out attachments' do
+ let(:project) { build_stubbed(:project, namespace: group, name: 'project') }
- it_behaves_like 'builds correct paths',
- store_dir: %r{ca/fe/fe/ed/\h+},
- absolute_path: %r{#{described_class.root}/ca/fe/fe/ed/secret/foo.jpg}
- end
+ include_examples 'builds correct paths',
+ store_dir: %r{@hashed/\h{2}/\h{2}/\h+},
+ upload_path: %r{\h+/<filename>}
+ end
- context 'when only repositories are rolled out' do
- let(:project) { build_stubbed(:project, namespace: group, name: 'project', storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) }
+ context 'when only repositories are rolled out' do
+ let(:project) { build_stubbed(:project, namespace: group, name: 'project', storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) }
- it_behaves_like 'builds correct legacy storage paths'
+ it_behaves_like 'builds correct legacy storage paths'
+ end
end
end
- context 'legacy storage' do
- it_behaves_like 'builds correct legacy storage paths'
- include_examples 'uses hashed storage'
+ context 'object store is remote' do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ # always use hashed storage path for remote uploads
+ it_behaves_like 'builds correct paths',
+ store_dir: %r{@hashed/\h{2}/\h{2}/\h+},
+ upload_path: %r{@hashed/\h{2}/\h{2}/\h+/\h+/<filename>}
end
describe 'initialize' do
@@ -78,6 +87,16 @@ describe FileUploader do
end
end
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')))
+ stub_uploads_object_storage
+ end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
+
describe '#upload=' do
let(:secret) { SecureRandom.hex }
let(:upload) { create(:upload, :issuable_upload, secret: secret, filename: 'file.txt') }
@@ -93,15 +112,5 @@ describe FileUploader do
uploader.upload = upload
end
-
- context 'uploader_context is empty' do
- it 'fallbacks to regex based extraction' do
- expect(upload).to receive(:uploader_context).and_return({})
-
- uploader.upload = upload
- expect(uploader.secret).to eq(secret)
- expect(uploader.instance_variable_get(:@identifier)).to eq('file.txt')
- end
- end
end
end
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
index 60e35dcf235..4fba122cce1 100644
--- a/spec/uploaders/gitlab_uploader_spec.rb
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -27,7 +27,7 @@ describe GitlabUploader do
describe '#file_cache_storage?' do
context 'when file storage is used' do
before do
- uploader_class.cache_storage(:file)
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::File }
end
it { is_expected.to be_file_cache_storage }
@@ -35,7 +35,7 @@ describe GitlabUploader do
context 'when is remote storage' do
before do
- uploader_class.cache_storage(:fog)
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::Fog }
end
it { is_expected.not_to be_file_cache_storage }
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index 5612ec7e661..42036d67f3d 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe JobArtifactUploader do
- let(:job_artifact) { create(:ci_job_artifact) }
+ let(:store) { described_class::Store::LOCAL }
+ let(:job_artifact) { create(:ci_job_artifact, file_store: store) }
let(:uploader) { described_class.new(job_artifact, :file) }
subject { uploader }
@@ -11,6 +12,17 @@ describe JobArtifactUploader do
cache_dir: %r[artifacts/tmp/cache],
work_dir: %r[artifacts/tmp/work]
+ context "object store is REMOTE" do
+ before do
+ stub_artifacts_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z]
+ end
+
describe '#open' do
subject { uploader.open }
@@ -36,6 +48,17 @@ describe JobArtifactUploader do
end
end
end
+
+ context 'when trace is stored in Object storage' do
+ before do
+ allow(uploader).to receive(:file_storage?) { false }
+ allow(uploader).to receive(:url) { 'http://object_storage.com/trace' }
+ end
+
+ it 'returns http io stream' do
+ is_expected.to be_a(Gitlab::Ci::Trace::HttpIO)
+ end
+ end
end
context 'file is stored in valid local_path' do
@@ -55,4 +78,14 @@ describe JobArtifactUploader do
it { is_expected.to include("/#{job_artifact.job_id}/#{job_artifact.id}/") }
it { is_expected.to end_with("ci_build_artifacts.zip") }
end
+
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/trace/sample_trace')))
+ stub_artifacts_object_storage
+ end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
end
diff --git a/spec/uploaders/legacy_artifact_uploader_spec.rb b/spec/uploaders/legacy_artifact_uploader_spec.rb
index 54c6a8b869b..eeb6fd90c9d 100644
--- a/spec/uploaders/legacy_artifact_uploader_spec.rb
+++ b/spec/uploaders/legacy_artifact_uploader_spec.rb
@@ -1,7 +1,8 @@
require 'rails_helper'
describe LegacyArtifactUploader do
- let(:job) { create(:ci_build) }
+ let(:store) { described_class::Store::LOCAL }
+ let(:job) { create(:ci_build, artifacts_file_store: store) }
let(:uploader) { described_class.new(job, :legacy_artifacts_file) }
let(:local_path) { described_class.root }
@@ -20,6 +21,17 @@ describe LegacyArtifactUploader do
cache_dir: %r[artifacts/tmp/cache],
work_dir: %r[artifacts/tmp/work]
+ context 'object store is remote' do
+ before do
+ stub_artifacts_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\d{4}_\d{1,2}/\d+/\d+\z]
+ end
+
describe '#filename' do
# we need to use uploader, as this makes to use mounter
# which initialises uploader.file object
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index 6ebc885daa8..a2fb3886610 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -11,4 +11,62 @@ describe LfsObjectUploader do
store_dir: %r[\h{2}/\h{2}],
cache_dir: %r[/lfs-objects/tmp/cache],
work_dir: %r[/lfs-objects/tmp/work]
+
+ context "object store is REMOTE" do
+ before do
+ stub_lfs_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}]
+ end
+
+ describe 'migration to object storage' do
+ context 'with object storage disabled' do
+ it "is skipped" do
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
+
+ lfs_object
+ end
+ end
+
+ context 'with object storage enabled' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
+
+ it 'is scheduled to run after creation' do
+ expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with(described_class.name, 'LfsObject', :file, kind_of(Numeric))
+
+ lfs_object
+ end
+ end
+ end
+
+ describe 'remote file' do
+ let(:remote) { described_class::Store::REMOTE }
+ let(:lfs_object) { create(:lfs_object, file_store: remote) }
+
+ context 'with object storage enabled' do
+ before do
+ stub_lfs_object_storage
+ end
+
+ it 'can store file remotely' do
+ allow(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async)
+
+ store_file(lfs_object)
+
+ expect(lfs_object.file_store).to eq remote
+ expect(lfs_object.file.path).not_to be_blank
+ end
+ end
+ end
+
+ def store_file(lfs_object)
+ lfs_object.file = fixture_file_upload(Rails.root.join("spec/fixtures/dk.png"), "`/png")
+ lfs_object.save!
+ end
end
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index 24a2fc0f72e..a8ba01d70b8 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -13,4 +13,26 @@ describe NamespaceFileUploader do
store_dir: %r[uploads/-/system/namespace/\d+],
upload_path: IDENTIFIER,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{IDENTIFIER}]
+
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[namespace/\d+/\h+],
+ upload_path: IDENTIFIER
+ end
+
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
+ stub_uploads_object_storage
+ end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
new file mode 100644
index 00000000000..1d406c71955
--- /dev/null
+++ b/spec/uploaders/object_storage_spec.rb
@@ -0,0 +1,630 @@
+require 'rails_helper'
+require 'carrierwave/storage/fog'
+
+class Implementation < GitlabUploader
+ include ObjectStorage::Concern
+ include ::RecordsUploads::Concern
+ prepend ::ObjectStorage::Extension::RecordsUploads
+
+ storage_options Gitlab.config.uploads
+
+ private
+
+ # user/:id
+ def dynamic_segment
+ File.join(model.class.to_s.underscore, model.id.to_s)
+ end
+end
+
+describe ObjectStorage do
+ let(:uploader_class) { Implementation }
+ let(:object) { build_stubbed(:user) }
+ let(:uploader) { uploader_class.new(object, :file) }
+
+ describe '#object_store=' do
+ before do
+ allow(uploader_class).to receive(:object_store_enabled?).and_return(true)
+ end
+
+ it "reload the local storage" do
+ uploader.object_store = described_class::Store::LOCAL
+ expect(uploader.file_storage?).to be_truthy
+ end
+
+ it "reload the REMOTE storage" do
+ uploader.object_store = described_class::Store::REMOTE
+ expect(uploader.file_storage?).to be_falsey
+ end
+
+ context 'object_store is Store::LOCAL' do
+ before do
+ uploader.object_store = described_class::Store::LOCAL
+ end
+
+ describe '#store_dir' do
+ it 'is the composition of (base_dir, dynamic_segment)' do
+ expect(uploader.store_dir).to start_with("uploads/-/system/user/")
+ end
+ end
+ end
+
+ context 'object_store is Store::REMOTE' do
+ before do
+ uploader.object_store = described_class::Store::REMOTE
+ end
+
+ describe '#store_dir' do
+ it 'is the composition of (dynamic_segment)' do
+ expect(uploader.store_dir).to start_with("user/")
+ end
+ end
+ end
+ end
+
+ describe '#object_store' do
+ it "delegates to <mount>_store on model" do
+ expect(object).to receive(:file_store)
+
+ uploader.object_store
+ end
+
+ context 'when store is null' do
+ before do
+ expect(object).to receive(:file_store).and_return(nil)
+ end
+
+ it "returns Store::LOCAL" do
+ expect(uploader.object_store).to eq(described_class::Store::LOCAL)
+ end
+ end
+
+ context 'when value is set' do
+ before do
+ expect(object).to receive(:file_store).and_return(described_class::Store::REMOTE)
+ end
+
+ it "returns the given value" do
+ expect(uploader.object_store).to eq(described_class::Store::REMOTE)
+ end
+ end
+ end
+
+ describe '#file_cache_storage?' do
+ context 'when file storage is used' do
+ before do
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::File }
+ end
+
+ it { expect(uploader).to be_file_cache_storage }
+ end
+
+ context 'when is remote storage' do
+ before do
+ expect(uploader_class).to receive(:cache_storage) { CarrierWave::Storage::Fog }
+ end
+
+ it { expect(uploader).not_to be_file_cache_storage }
+ end
+ end
+
+ # this means the model shall include
+ # include RecordsUpload::Concern
+ # prepend ObjectStorage::Extension::RecordsUploads
+ # the object_store persistence is delegated to the `Upload` model.
+ #
+ context 'when persist_object_store? is false' do
+ let(:object) { create(:project, :with_avatar) }
+ let(:uploader) { object.avatar }
+
+ it { expect(object).to be_a(Avatarable) }
+ it { expect(uploader.persist_object_store?).to be_falsey }
+
+ describe 'delegates the object_store logic to the `Upload` model' do
+ it 'sets @upload to the found `upload`' do
+ expect(uploader.upload).to eq(uploader.upload)
+ end
+
+ it 'sets @object_store to the `Upload` value' do
+ expect(uploader.object_store).to eq(uploader.upload.store)
+ end
+ end
+
+ describe '#migrate!' do
+ let(:new_store) { ObjectStorage::Store::REMOTE }
+
+ before do
+ stub_uploads_object_storage(uploader: AvatarUploader)
+ end
+
+ subject { uploader.migrate!(new_store) }
+
+ it 'persist @object_store to the recorded upload' do
+ subject
+
+ expect(uploader.upload.store).to eq(new_store)
+ end
+
+ describe 'fails' do
+ it 'is handled gracefully' do
+ store = uploader.object_store
+ expect_any_instance_of(Upload).to receive(:save!).and_raise("An error")
+
+ expect { subject }.to raise_error("An error")
+ expect(uploader.exists?).to be_truthy
+ expect(uploader.upload.store).to eq(store)
+ end
+ end
+ end
+ end
+
+ # this means the model holds an <mounted_as>_store attribute directly
+ # and do not delegate the object_store persistence to the `Upload` model.
+ #
+ context 'persist_object_store? is true' do
+ context 'when using JobArtifactsUploader' do
+ let(:store) { described_class::Store::LOCAL }
+ let(:object) { create(:ci_job_artifact, :archive, file_store: store) }
+ let(:uploader) { object.file }
+
+ context 'checking described_class' do
+ it "uploader include described_class::Concern" do
+ expect(uploader).to be_a(described_class::Concern)
+ end
+ end
+
+ describe '#use_file' do
+ context 'when file is stored locally' do
+ it "calls a regular path" do
+ expect { |b| uploader.use_file(&b) }.not_to yield_with_args(%r[tmp/cache])
+ end
+ end
+
+ context 'when file is stored remotely' do
+ let(:store) { described_class::Store::REMOTE }
+
+ before do
+ stub_artifacts_object_storage
+ end
+
+ it "calls a cache path" do
+ expect { |b| uploader.use_file(&b) }.to yield_with_args(%r[tmp/cache])
+ end
+ end
+ end
+
+ describe '#migrate!' do
+ subject { uploader.migrate!(new_store) }
+
+ shared_examples "updates the underlying <mounted>_store" do
+ it do
+ subject
+
+ expect(object.file_store).to eq(new_store)
+ end
+ end
+
+ context 'when using the same storage' do
+ let(:new_store) { store }
+
+ it "to not migrate the storage" do
+ subject
+
+ expect(uploader).not_to receive(:store!)
+ expect(uploader.object_store).to eq(store)
+ end
+ end
+
+ context 'when migrating to local storage' do
+ let(:store) { described_class::Store::REMOTE }
+ let(:new_store) { described_class::Store::LOCAL }
+
+ before do
+ stub_artifacts_object_storage
+ end
+
+ include_examples "updates the underlying <mounted>_store"
+
+ it "local file does not exist" do
+ expect(File.exist?(uploader.path)).to eq(false)
+ end
+
+ it "remote file exist" do
+ expect(uploader.file.exists?).to be_truthy
+ end
+
+ it "does migrate the file" do
+ subject
+
+ expect(uploader.object_store).to eq(new_store)
+ expect(File.exist?(uploader.path)).to eq(true)
+ end
+ end
+
+ context 'when migrating to remote storage' do
+ let(:new_store) { described_class::Store::REMOTE }
+ let!(:current_path) { uploader.path }
+
+ it "file does exist" do
+ expect(File.exist?(current_path)).to eq(true)
+ end
+
+ context 'when storage is disabled' do
+ before do
+ stub_artifacts_object_storage(enabled: false)
+ end
+
+ it "to raise an error" do
+ expect { subject }.to raise_error(/Object Storage is not enabled/)
+ end
+ end
+
+ context 'when credentials are set' do
+ before do
+ stub_artifacts_object_storage
+ end
+
+ include_examples "updates the underlying <mounted>_store"
+
+ it "does migrate the file" do
+ subject
+
+ expect(uploader.object_store).to eq(new_store)
+ end
+
+ it "does delete original file" do
+ subject
+
+ expect(File.exist?(current_path)).to eq(false)
+ end
+
+ context 'when subject save fails' do
+ before do
+ expect(uploader).to receive(:persist_object_store!).and_raise(RuntimeError, "exception")
+ end
+
+ it "original file is not removed" do
+ expect { subject }.to raise_error(/exception/)
+
+ expect(File.exist?(current_path)).to eq(true)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '#fog_directory' do
+ let(:remote_directory) { 'directory' }
+
+ before do
+ allow(uploader_class).to receive(:options) do
+ double(object_store: double(remote_directory: remote_directory))
+ end
+ end
+
+ subject { uploader.fog_directory }
+
+ it { is_expected.to eq(remote_directory) }
+ end
+
+ describe '#fog_credentials' do
+ let(:connection) { Settingslogic.new("provider" => "AWS") }
+
+ before do
+ allow(uploader_class).to receive(:options) do
+ double(object_store: double(connection: connection))
+ end
+ end
+
+ subject { uploader.fog_credentials }
+
+ it { is_expected.to eq(provider: 'AWS') }
+ end
+
+ describe '#fog_public' do
+ subject { uploader.fog_public }
+
+ it { is_expected.to eq(false) }
+ end
+
+ describe '.workhorse_authorize' do
+ subject { uploader_class.workhorse_authorize }
+
+ before do
+ # ensure that we use regular Fog libraries
+ # other tests might call `Fog.mock!` and
+ # it will make tests to fail
+ Fog.unmock!
+ end
+
+ shared_examples 'uses local storage' do
+ it "returns temporary path" do
+ is_expected.to have_key(:TempPath)
+
+ expect(subject[:TempPath]).to start_with(uploader_class.root)
+ expect(subject[:TempPath]).to include(described_class::TMP_UPLOAD_PATH)
+ end
+
+ it "does not return remote store" do
+ is_expected.not_to have_key('RemoteObject')
+ end
+ end
+
+ shared_examples 'uses remote storage' do
+ it "returns remote store" do
+ is_expected.to have_key(:RemoteObject)
+
+ expect(subject[:RemoteObject]).to have_key(:ID)
+ expect(subject[:RemoteObject]).to have_key(:GetURL)
+ expect(subject[:RemoteObject]).to have_key(:DeleteURL)
+ expect(subject[:RemoteObject]).to have_key(:StoreURL)
+ expect(subject[:RemoteObject][:GetURL]).to include(described_class::TMP_UPLOAD_PATH)
+ expect(subject[:RemoteObject][:DeleteURL]).to include(described_class::TMP_UPLOAD_PATH)
+ expect(subject[:RemoteObject][:StoreURL]).to include(described_class::TMP_UPLOAD_PATH)
+ end
+
+ it "does not return local store" do
+ is_expected.not_to have_key('TempPath')
+ end
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:enabled) { false }
+ end
+
+ it_behaves_like 'uses local storage'
+ end
+
+ context 'when object storage is enabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:enabled) { true }
+ end
+
+ context 'when direct upload is enabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:direct_upload) { true }
+ end
+
+ context 'uses AWS' do
+ before do
+ expect(uploader_class).to receive(:object_store_credentials) do
+ { provider: "AWS",
+ aws_access_key_id: "AWS_ACCESS_KEY_ID",
+ aws_secret_access_key: "AWS_SECRET_ACCESS_KEY",
+ region: "eu-central-1" }
+ end
+ end
+
+ it_behaves_like 'uses remote storage' do
+ let(:storage_url) { "https://uploads.s3-eu-central-1.amazonaws.com/" }
+
+ it 'returns links for S3' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+
+ context 'uses Google' do
+ before do
+ expect(uploader_class).to receive(:object_store_credentials) do
+ { provider: "Google",
+ google_storage_access_key_id: 'ACCESS_KEY_ID',
+ google_storage_secret_access_key: 'SECRET_ACCESS_KEY' }
+ end
+ end
+
+ it_behaves_like 'uses remote storage' do
+ let(:storage_url) { "https://storage.googleapis.com/uploads/" }
+
+ it 'returns links for Google Cloud' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+
+ context 'uses GDK/minio' do
+ before do
+ expect(uploader_class).to receive(:object_store_credentials) do
+ { provider: "AWS",
+ aws_access_key_id: "AWS_ACCESS_KEY_ID",
+ aws_secret_access_key: "AWS_SECRET_ACCESS_KEY",
+ endpoint: 'http://127.0.0.1:9000',
+ path_style: true,
+ region: "gdk" }
+ end
+ end
+
+ it_behaves_like 'uses remote storage' do
+ let(:storage_url) { "http://127.0.0.1:9000/uploads/" }
+
+ it 'returns links for S3' do
+ expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url)
+ expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url)
+ end
+ end
+ end
+ end
+
+ context 'when direct upload is disabled' do
+ before do
+ allow(Gitlab.config.uploads.object_store).to receive(:direct_upload) { false }
+ end
+
+ it_behaves_like 'uses local storage'
+ end
+ end
+ end
+
+ describe '#store_workhorse_file!' do
+ subject do
+ uploader.store_workhorse_file!(params, :file)
+ end
+
+ context 'when local file is used' do
+ context 'when valid file is used' do
+ let(:target_path) do
+ File.join(uploader_class.root, uploader_class::TMP_UPLOAD_PATH)
+ end
+
+ before do
+ FileUtils.mkdir_p(target_path)
+ end
+
+ context 'when no filename is specified' do
+ let(:params) do
+ { "file.path" => "test/file" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing filename/)
+ end
+ end
+
+ context 'when invalid file is specified' do
+ let(:file_path) do
+ File.join(target_path, "..", "test.file")
+ end
+
+ before do
+ FileUtils.touch(file_path)
+ end
+
+ let(:params) do
+ { "file.path" => file_path,
+ "file.name" => "my_file.txt" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file path/)
+ end
+ end
+
+ context 'when filename is specified' do
+ let(:params) do
+ { "file.path" => tmp_file,
+ "file.name" => "my_file.txt" }
+ end
+
+ let(:tmp_file) { Tempfile.new('filename', target_path) }
+
+ before do
+ FileUtils.touch(tmp_file)
+ end
+
+ after do
+ FileUtils.rm_f(tmp_file)
+ end
+
+ it 'succeeds' do
+ expect { subject }.not_to raise_error
+
+ expect(uploader).to be_exists
+ end
+
+ it 'proper path is being used' do
+ subject
+
+ expect(uploader.path).to start_with(uploader_class.root)
+ expect(uploader.path).to end_with("my_file.txt")
+ end
+
+ it 'source file to not exist' do
+ subject
+
+ expect(File.exist?(tmp_file.path)).to be_falsey
+ end
+ end
+ end
+ end
+
+ context 'when remote file is used' do
+ let!(:fog_connection) do
+ stub_uploads_object_storage(uploader_class)
+ end
+
+ context 'when valid file is used' do
+ context 'when no filename is specified' do
+ let(:params) do
+ { "file.remote_id" => "test/123123" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing filename/)
+ end
+ end
+
+ context 'when invalid file is specified' do
+ let(:params) do
+ { "file.remote_id" => "../test/123123",
+ "file.name" => "my_file.txt" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file path/)
+ end
+ end
+
+ context 'when non existing file is specified' do
+ let(:params) do
+ { "file.remote_id" => "test/12312300",
+ "file.name" => "my_file.txt" }
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing file/)
+ end
+ end
+
+ context 'when filename is specified' do
+ let(:params) do
+ { "file.remote_id" => "test/123123",
+ "file.name" => "my_file.txt" }
+ end
+
+ let!(:fog_file) do
+ fog_connection.directories.get('uploads').files.create(
+ key: 'tmp/upload/test/123123',
+ body: 'content'
+ )
+ end
+
+ it 'succeeds' do
+ expect { subject }.not_to raise_error
+
+ expect(uploader).to be_exists
+ end
+
+ it 'path to not be temporary' do
+ subject
+
+ expect(uploader.path).not_to be_nil
+ expect(uploader.path).not_to include('tmp/upload')
+ expect(uploader.url).to include('/my_file.txt')
+ end
+
+ it 'url is used' do
+ subject
+
+ expect(uploader.url).not_to be_nil
+ expect(uploader.url).to include('/my_file.txt')
+ end
+ end
+ end
+ end
+
+ context 'when no file is used' do
+ let(:params) { {} }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file/)
+ end
+ end
+ end
+end
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index ed1fba6edda..c70521d90dc 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -14,6 +14,18 @@ describe PersonalFileUploader do
upload_path: IDENTIFIER,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{IDENTIFIER}]
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[\d+/\h+],
+ upload_path: IDENTIFIER
+ end
+
describe '#to_h' do
before do
subject.instance_variable_set(:@secret, 'secret')
@@ -30,4 +42,14 @@ describe PersonalFileUploader do
)
end
end
+
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt')))
+ stub_uploads_object_storage
+ end
+
+ it_behaves_like "migrates", to_store: described_class::Store::REMOTE
+ it_behaves_like "migrates", from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
end
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/projects/ci/lints/show.html.haml_spec.rb
index 7724d54c569..2f0cd38c14a 100644
--- a/spec/views/ci/lints/show.html.haml_spec.rb
+++ b/spec/views/projects/ci/lints/show.html.haml_spec.rb
@@ -1,11 +1,13 @@
require 'spec_helper'
-describe 'ci/lints/show' do
+describe 'projects/ci/lints/show' do
include Devise::Test::ControllerHelpers
+ let(:project) { create(:project, :repository) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
describe 'XSS protection' do
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
before do
+ assign(:project, project)
assign(:status, true)
assign(:builds, config_processor.builds)
assign(:stages, config_processor.stages)
@@ -47,22 +49,21 @@ describe 'ci/lints/show' do
end
end
- let(:content) do
- {
- build_template: {
- script: './build.sh',
- tags: ['dotnet'],
- only: ['test@dude/repo'],
- except: ['deploy'],
- environment: 'testing'
+ context 'when the content is valid' do
+ let(:content) do
+ {
+ build_template: {
+ script: './build.sh',
+ tags: ['dotnet'],
+ only: ['test@dude/repo'],
+ except: ['deploy'],
+ environment: 'testing'
+ }
}
- }
- end
-
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
+ end
- context 'when the content is valid' do
before do
+ assign(:project, project)
assign(:status, true)
assign(:builds, config_processor.builds)
assign(:stages, config_processor.stages)
@@ -82,6 +83,7 @@ describe 'ci/lints/show' do
context 'when the content is invalid' do
before do
+ assign(:project, project)
assign(:status, false)
assign(:error, 'Undefined error')
end
diff --git a/spec/views/projects/diffs/_stats.html.haml_spec.rb b/spec/views/projects/diffs/_stats.html.haml_spec.rb
new file mode 100644
index 00000000000..c7d2f85747c
--- /dev/null
+++ b/spec/views/projects/diffs/_stats.html.haml_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'projects/diffs/_stats.html.haml' do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
+
+ def render_view
+ render partial: "projects/diffs/stats", locals: { diff_files: commit.diffs.diff_files }
+ end
+
+ context 'when the commit contains several changes' do
+ it 'uses plural for additions' do
+ render_view
+
+ expect(rendered).to have_text('additions')
+ end
+
+ it 'uses plural for deletions' do
+ render_view
+ end
+ end
+
+ context 'when the commit contains no addition and no deletions' do
+ let(:commit) { project.commit('4cd80ccab63c82b4bad16faa5193fbd2aa06df40') }
+
+ it 'uses plural for additions' do
+ render_view
+
+ expect(rendered).to have_text('additions')
+ end
+
+ it 'uses plural for deletions' do
+ render_view
+
+ expect(rendered).to have_text('deletions')
+ end
+ end
+
+ context 'when the commit contains exactly one addition and one deletion' do
+ let(:commit) { project.commit('08f22f255f082689c0d7d39d19205085311542bc') }
+
+ it 'uses singular for additions' do
+ render_view
+
+ expect(rendered).to have_text('addition')
+ expect(rendered).not_to have_text('additions')
+ end
+
+ it 'uses singular for deletions' do
+ render_view
+
+ expect(rendered).to have_text('deletion')
+ expect(rendered).not_to have_text('deletions')
+ end
+ end
+end
diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb
new file mode 100644
index 00000000000..85167bca115
--- /dev/null
+++ b/spec/views/projects/services/_form.haml_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe 'projects/services/_form' do
+ let(:project) { create(:redmine_project) }
+ let(:user) { create(:admin) }
+
+ before do
+ assign(:project, project)
+
+ allow(controller).to receive(:current_user).and_return(user)
+
+ allow(view).to receive_messages(current_user: user,
+ can?: true,
+ current_application_settings: Gitlab::CurrentSettings.current_application_settings)
+ end
+
+ context 'commit_events and merge_request_events' do
+ before do
+ assign(:service, project.redmine_service)
+ end
+
+ it 'display merge_request_events and commit_events descriptions' do
+ allow(RedmineService).to receive(:supported_events).and_return(%w(commit merge_request))
+
+ render
+
+ expect(rendered).to have_content('Event will be triggered when a commit is created/updated')
+ expect(rendered).to have_content('Event will be triggered when a merge request is created/updated/merged')
+ end
+
+ context 'when service is JIRA' do
+ let(:project) { create(:jira_project) }
+
+ before do
+ assign(:service, project.jira_service)
+ end
+
+ it 'display merge_request_events and commit_events descriptions' do
+ render
+
+ expect(rendered).to have_content('JIRA comments will be created when an issue gets referenced in a commit.')
+ expect(rendered).to have_content('JIRA comments will be created when an issue gets referenced in a merge request.')
+ end
+ end
+ end
+end
diff --git a/spec/workers/object_storage_upload_worker_spec.rb b/spec/workers/object_storage_upload_worker_spec.rb
new file mode 100644
index 00000000000..32ddcbe9757
--- /dev/null
+++ b/spec/workers/object_storage_upload_worker_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe ObjectStorageUploadWorker do
+ let(:local) { ObjectStorage::Store::LOCAL }
+ let(:remote) { ObjectStorage::Store::REMOTE }
+
+ def perform
+ described_class.perform_async(uploader_class.name, subject_class, file_field, subject_id)
+ end
+
+ context 'for LFS' do
+ let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) }
+ let(:uploader_class) { LfsObjectUploader }
+ let(:subject_class) { LfsObject }
+ let(:file_field) { :file }
+ let(:subject_id) { lfs_object.id }
+
+ context 'when object storage is enabled' do
+ before do
+ stub_lfs_object_storage(background_upload: true)
+ end
+
+ it 'uploads object to storage' do
+ expect { perform }.to change { lfs_object.reload.file_store }.from(local).to(remote)
+ end
+
+ context 'when background upload is disabled' do
+ before do
+ allow(Gitlab.config.lfs.object_store).to receive(:background_upload) { false }
+ end
+
+ it 'is skipped' do
+ expect { perform }.not_to change { lfs_object.reload.file_store }
+ end
+ end
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ stub_lfs_object_storage(enabled: false)
+ end
+
+ it "doesn't migrate files" do
+ perform
+
+ expect(lfs_object.reload.file_store).to eq(local)
+ end
+ end
+ end
+
+ context 'for legacy artifacts' do
+ let(:build) { create(:ci_build, :legacy_artifacts) }
+ let(:uploader_class) { LegacyArtifactUploader }
+ let(:subject_class) { Ci::Build }
+ let(:file_field) { :artifacts_file }
+ let(:subject_id) { build.id }
+
+ context 'when local storage is used' do
+ let(:store) { local }
+
+ context 'and remote storage is defined' do
+ before do
+ stub_artifacts_object_storage(background_upload: true)
+ end
+
+ it "migrates file to remote storage" do
+ perform
+
+ expect(build.reload.artifacts_file_store).to eq(remote)
+ end
+
+ context 'for artifacts_metadata' do
+ let(:file_field) { :artifacts_metadata }
+
+ it 'migrates metadata to remote storage' do
+ perform
+
+ expect(build.reload.artifacts_metadata_store).to eq(remote)
+ end
+ end
+ end
+ end
+ end
+
+ context 'for job artifacts' do
+ let(:artifact) { create(:ci_job_artifact, :archive) }
+ let(:uploader_class) { JobArtifactUploader }
+ let(:subject_class) { Ci::JobArtifact }
+ let(:file_field) { :file }
+ let(:subject_id) { artifact.id }
+
+ context 'when local storage is used' do
+ let(:store) { local }
+
+ context 'and remote storage is defined' do
+ before do
+ stub_artifacts_object_storage(background_upload: true)
+ end
+
+ it "migrates file to remote storage" do
+ perform
+
+ expect(artifact.reload.file_store).to eq(remote)
+ end
+ end
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/peek.js b/vendor/assets/javascripts/peek.js
deleted file mode 100644
index 7c6d226fa6a..00000000000
--- a/vendor/assets/javascripts/peek.js
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * this is a modified version of https://github.com/peek/peek/blob/master/app/assets/javascripts/peek.js
- *
- * - Removed the dependency on jquery.tipsy
- * - Removed the initializeTipsy and toggleBar functions
- * - Customized updatePerformanceBar to handle SQL query and Gitaly call lists
- * - Changed /peek/results to /-/peek/results
- * - Removed the keypress, pjax:end, page:change, and turbolinks:load handlers
- */
-(function($) {
- var fetchRequestResults, getRequestId, peekEnabled, updatePerformanceBar, createTable, createTableRow;
- getRequestId = function() {
- return $('#peek').data('requestId');
- };
- peekEnabled = function() {
- return $('#peek').length;
- };
- updatePerformanceBar = function(results) {
- Object.keys(results.data).forEach(function(key) {
- Object.keys(results.data[key]).forEach(function(label) {
- var data = results.data[key][label];
- var table = createTable(key, label, data);
- var target = $('[data-defer-to="' + key + '-' + label + '"]');
-
- if (table) {
- target.html(table);
- } else {
- target.text(data);
- }
- });
- });
- return $(document).trigger('peek:render', [getRequestId(), results]);
- };
- createTable = function(key, label, data) {
- if (label !== 'queries' && label !== 'details') {
- return;
- }
-
- var table = document.createElement('table');
-
- for (var i = 0; i < data.length; i += 1) {
- table.appendChild(createTableRow(data[i]));
- }
-
- table.className = 'table';
-
- return table;
- };
- createTableRow = function(row) {
- var tr = document.createElement('tr');
- var durationTd = document.createElement('td');
- var strong = document.createElement('strong');
-
- strong.append(row['duration'] + 'ms');
- durationTd.appendChild(strong);
- tr.appendChild(durationTd);
-
- ['sql', 'feature', 'enabled', 'request'].forEach(function(key) {
- if (!row[key]) { return; }
-
- var td = document.createElement('td');
-
- td.appendChild(document.createTextNode(row[key]));
- tr.appendChild(td);
- });
-
- return tr;
- };
- fetchRequestResults = function() {
- return $.ajax('/-/peek/results', {
- data: {
- request_id: getRequestId()
- },
- success: function(data, textStatus, xhr) {
- return updatePerformanceBar(data);
- },
- error: function(xhr, textStatus, error) {}
- });
- };
- $(document).on('peek:update', fetchRequestResults);
- return $(function() {
- if (peekEnabled()) {
- return $(this).trigger('peek:update');
- }
- });
-})(jQuery);
diff --git a/yarn.lock b/yarn.lock
index 3cc5445c402..584951b5da0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -54,9 +54,9 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.14.0":
- version "1.14.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.14.0.tgz#b4a5cca3106f33224c5486cf674ba3b70cee727e"
+"@gitlab-org/gitlab-svgs@^1.16.0":
+ version "1.16.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.16.0.tgz#6c88a1bd9f5b3d3e5bf6a6d89d61724022185667"
"@types/jquery@^2.0.40":
version "2.0.48"
@@ -3605,7 +3605,7 @@ fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
-fsevents@^1.0.0:
+fsevents@^1.0.0, fsevents@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
dependencies:
@@ -4232,7 +4232,7 @@ ignore@^3.2.0:
version "3.3.3"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
-ignore@^3.3.5:
+ignore@^3.3.5, ignore@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"