summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock2
-rw-r--r--INSTALLATION_TYPE1
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_canceled.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_created.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_failed.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_manual.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_not_found.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_pending.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_running.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_skipped.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_success.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_warning.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_canceled.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_canceled.pngbin0 -> 864 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_created.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_created.pngbin0 -> 889 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_failed.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_failed.pngbin0 -> 1015 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_manual.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_manual.pngbin0 -> 1067 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_not_found.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_not_found.pngbin0 -> 945 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_pending.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_pending.pngbin0 -> 919 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_running.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_running.pngbin0 -> 1077 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_skipped.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_skipped.pngbin0 -> 923 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_success.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_success.pngbin0 -> 1044 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_warning.icobin4286 -> 0 bytes
-rw-r--r--app/assets/images/ci_favicons/favicon_status_warning.pngbin0 -> 830 bytes
-rw-r--r--app/assets/images/favicon-blue.pngbin0 -> 1522 bytes
-rw-r--r--app/assets/images/favicon-yellow.icobin5430 -> 0 bytes
-rw-r--r--app/assets/images/favicon-yellow.pngbin0 -> 1667 bytes
-rw-r--r--app/assets/images/favicon.icobin5430 -> 0 bytes
-rw-r--r--app/assets/images/favicon.pngbin0 -> 1611 bytes
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue41
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue4
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js1
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/boards/models/assignee.js12
-rw-r--r--app/assets/javascripts/boards/models/list.js87
-rw-r--r--app/assets/javascripts/boards/services/board_service.js10
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js24
-rw-r--r--app/assets/javascripts/gl_dropdown.js6
-rw-r--r--app/assets/javascripts/group_label_subscription.js18
-rw-r--r--app/assets/javascripts/label_manager.js15
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js51
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js113
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js14
-rw-r--r--app/assets/javascripts/project_label_subscription.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js2
-rw-r--r--app/assets/javascripts/vue_shared/models/assignee.js13
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss11
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss10
-rw-r--r--app/assets/stylesheets/framework/tables.scss84
-rw-r--r--app/assets/stylesheets/framework/variables.scss18
-rw-r--r--app/assets/stylesheets/pages/builds.scss16
-rw-r--r--app/assets/stylesheets/pages/labels.scss229
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss86
-rw-r--r--app/assets/stylesheets/pages/search.scss3
-rw-r--r--app/assets/stylesheets/pages/settings.scss10
-rw-r--r--app/assets/stylesheets/pages/settings_ci_cd.scss9
-rw-r--r--app/assets/stylesheets/performance_bar.scss1
-rw-r--r--app/controllers/admin/appearances_controller.rb9
-rw-r--r--app/controllers/boards/lists_controller.rb14
-rw-r--r--app/controllers/concerns/issuable_actions.rb30
-rw-r--r--app/controllers/concerns/issuable_collections.rb7
-rw-r--r--app/controllers/concerns/uploads_actions.rb7
-rw-r--r--app/controllers/groups/labels_controller.rb23
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb2
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/finders/group_members_finder.rb26
-rw-r--r--app/finders/issuable_finder.rb39
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/members_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb4
-rw-r--r--app/helpers/favicon_helper.rb7
-rw-r--r--app/helpers/labels_helper.rb8
-rw-r--r--app/helpers/page_layout_helper.rb5
-rw-r--r--app/models/appearance.rb1
-rw-r--r--app/models/group.rb29
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/list.rb18
-rw-r--r--app/models/notification_recipient.rb31
-rw-r--r--app/models/project_auto_devops.rb31
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/user.rb5
-rw-r--r--app/serializers/status_entity.rb11
-rw-r--r--app/services/applications/create_service.rb2
-rw-r--r--app/services/boards/issues/list_service.rb21
-rw-r--r--app/services/boards/issues/move_service.rb6
-rw-r--r--app/services/boards/lists/create_service.rb20
-rw-r--r--app/uploaders/favicon_uploader.rb24
-rw-r--r--app/uploaders/uploader_helper.rb2
-rw-r--r--app/views/admin/appearances/_form.html.haml23
-rw-r--r--app/views/admin/groups/_form.html.haml7
-rw-r--r--app/views/admin/groups/_group.html.haml3
-rw-r--r--app/views/admin/groups/show.html.haml8
-rw-r--r--app/views/admin/services/_form.html.haml3
-rw-r--r--app/views/groups/labels/index.html.haml45
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml2
-rw-r--r--app/views/projects/clusters/user/_form.html.haml12
-rw-r--r--app/views/projects/clusters/user/_show.html.haml10
-rw-r--r--app/views/projects/labels/index.html.haml34
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml44
-rw-r--r--app/views/projects/project_members/_groups.html.haml2
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml4
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml27
-rw-r--r--app/views/projects/services/prometheus/_configuration_banner.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_help.html.haml2
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml169
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml81
-rw-r--r--app/views/shared/_field.html.haml6
-rw-r--r--app/views/shared/_label.html.haml133
-rw-r--r--app/views/shared/_label_row.html.haml35
-rw-r--r--app/views/shared/_service_settings.html.haml24
-rw-r--r--app/views/shared/boards/components/_board.html.haml12
-rw-r--r--app/views/shared/issuable/_board_create_list_dropdown.html.haml8
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml3
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml7
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml9
-rw-r--r--app/views/shared/members/_group.html.haml18
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml2
-rw-r--r--app/workers/storage_migrator_worker.rb25
-rw-r--r--changelogs/unreleased/38542-application-control-panel-in-settings-page.yml5
-rw-r--r--changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml5
-rw-r--r--changelogs/unreleased/44267-improve-failed-jobs-tab.yml5
-rw-r--r--changelogs/unreleased/44790-disabled-emails-logging.yml5
-rw-r--r--changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml5
-rw-r--r--changelogs/unreleased/46648-timeout-searching-group-issues.yml5
-rw-r--r--changelogs/unreleased/46922-hashed-storage-single-project.yml5
-rw-r--r--changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml5
-rw-r--r--changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml5
-rw-r--r--changelogs/unreleased/feature-customizable-favicon.yml5
-rw-r--r--changelogs/unreleased/ide-url-util-relative-url-fix.yml6
-rw-r--r--changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml5
-rw-r--r--changelogs/unreleased/issue_44230.yml5
-rw-r--r--changelogs/unreleased/n-plus-one-notification-recipients.yml5
-rw-r--r--config/initializers/disable_email_interceptor.rb5
-rw-r--r--config/initializers/mime_types.rb2
-rw-r--r--config/initializers/mini_magick.rb3
-rw-r--r--config/locales/carrierwave.en.yml14
-rw-r--r--config/prometheus/additional_metrics.yml8
-rw-r--r--config/routes/admin.rb1
-rw-r--r--config/routes/uploads.rb2
-rw-r--r--db/fixtures/development/04_project.rb4
-rw-r--r--db/fixtures/development/08_settings.rb7
-rw-r--r--db/migrate/20170925184228_add_favicon_to_appearances.rb7
-rw-r--r--db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb19
-rw-r--r--db/schema.rb2
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/administration/raketasks/storage.md45
-rw-r--r--doc/ci/environments.md4
-rw-r--r--doc/ci/examples/README.md4
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.pngbin0 -> 49735 bytes
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.pngbin0 -> 82121 bytes
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md142
-rw-r--r--doc/customization/favicon.md16
-rw-r--r--doc/customization/favicon/appearance.pngbin0 -> 52245 bytes
-rw-r--r--doc/customization/favicon/custom_favicon.pngbin0 -> 60083 bytes
-rw-r--r--doc/topics/autodevops/img/autodevops_domain_variables.pngbin0 -> 8456 bytes
-rw-r--r--doc/topics/autodevops/img/autodevops_multiple_clusters.pngbin0 -> 12619 bytes
-rw-r--r--doc/topics/autodevops/index.md143
-rw-r--r--doc/user/gitlab_com/index.md1
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/index.md2
-rw-r--r--doc/user/project/clusters/index.md4
-rw-r--r--doc/user/project/import/github.md4
-rw-r--r--doc/user/project/integrations/prometheus.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress.md2
-rw-r--r--doc/user/reserved_names.md2
-rw-r--r--doc/workflow/notifications.md9
-rw-r--r--lib/api/helpers/runner.rb5
-rw-r--r--lib/api/runner.rb6
-rw-r--r--lib/backup/files.rb11
-rw-r--r--lib/gitlab.rb1
-rw-r--r--lib/gitlab/current_settings.rb15
-rw-r--r--lib/gitlab/favicon.rb47
-rw-r--r--lib/gitlab/git/lfs_changes.rb26
-rw-r--r--lib/gitlab/git/rev_list.rb3
-rw-r--r--lib/gitlab/hashed_storage/migrator.rb57
-rw-r--r--lib/gitlab/hashed_storage/rake_helper.rb14
-rw-r--r--lib/gitlab/path_regex.rb2
-rw-r--r--lib/gitlab/themes.rb16
-rw-r--r--lib/gitlab/usage_data.rb1
-rw-r--r--lib/tasks/gitlab/storage.rake21
-rw-r--r--locale/gitlab.pot2
-rw-r--r--public/favicon.icobin5430 -> 0 bytes
-rw-r--r--public/favicon.pngbin0 -> 1611 bytes
-rw-r--r--qa/qa/git/repository.rb17
-rw-r--r--qa/qa/page/project/pipeline/show.rb4
-rw-r--r--qa/qa/specs/features/repository/protected_branches_spec.rb4
-rw-r--r--qa/spec/git/repository_spec.rb40
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb2
-rw-r--r--spec/controllers/uploads_controller_spec.rb104
-rw-r--r--spec/factories/project_auto_devops.rb9
-rw-r--r--spec/features/admin/admin_appearance_spec.rb20
-rw-r--r--spec/features/admin/admin_uses_repository_checks_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb2
-rw-r--r--spec/features/issues/form_spec.rb3
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb2
-rw-r--r--spec/features/projects/labels/subscription_spec.rb6
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb8
-rw-r--r--spec/features/projects/labels/user_removes_labels_spec.rb20
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb10
-rw-r--r--spec/finders/group_members_finder_spec.rb12
-rw-r--r--spec/finders/members_finder_spec.rb12
-rw-r--r--spec/fixtures/api/schemas/list.json2
-rw-r--r--spec/helpers/page_layout_helper_spec.rb17
-rw-r--r--spec/helpers/preferences_helper_spec.rb2
-rw-r--r--spec/javascripts/boards/board_card_spec.js2
-rw-r--r--spec/javascripts/boards/boards_store_spec.js2
-rw-r--r--spec/javascripts/boards/issue_card_spec.js2
-rw-r--r--spec/javascripts/boards/issue_spec.js2
-rw-r--r--spec/javascripts/boards/list_spec.js2
-rw-r--r--spec/javascripts/boards/modal_store_spec.js2
-rw-r--r--spec/javascripts/datetime_utility_spec.js23
-rw-r--r--spec/javascripts/jobs/mock_data.js4
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js47
-rw-r--r--spec/javascripts/lib/utils/mock_data.js5
-rw-r--r--spec/javascripts/lib/utils/url_utility_spec.js29
-rw-r--r--spec/javascripts/pipelines/graph/mock_data.js18
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js12
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js13
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb143
-rw-r--r--spec/lib/gitlab/favicon_spec.rb52
-rw-r--r--spec/lib/gitlab/hashed_storage/migrator_spec.rb75
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/themes_spec.rb4
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb2
-rw-r--r--spec/models/group_spec.rb26
-rw-r--r--spec/models/notification_recipient_spec.rb44
-rw-r--r--spec/models/project_auto_devops_spec.rb121
-rw-r--r--spec/models/project_services/jira_service_spec.rb19
-rw-r--r--spec/models/project_spec.rb2
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/requests/api/runner_spec.rb30
-rw-r--r--spec/serializers/build_serializer_spec.rb4
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
-rw-r--r--spec/serializers/status_entity_spec.rb12
-rw-r--r--spec/services/notification_recipient_service_spec.rb36
-rw-r--r--spec/services/notification_service_spec.rb188
-rw-r--r--spec/services/system_note_service_spec.rb9
-rw-r--r--spec/support/helpers/assets_helpers.rb15
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb45
-rw-r--r--spec/uploaders/favicon_uploader_spec.rb29
-rw-r--r--spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb8
-rw-r--r--spec/workers/storage_migrator_worker_spec.rb25
258 files changed, 3126 insertions, 1085 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 1679ae378c9..e366538d907 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
.dedicated-runner: &dedicated-runner
retry: 1
diff --git a/Gemfile b/Gemfile
index 339761bf8fa..f8908ded9b3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -108,6 +108,7 @@ gem 'hamlit', '~> 2.6.1'
# Files attachments
gem 'carrierwave', '~> 1.2'
+gem 'mini_magick'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index ff9e81bdc9f..5f8d1a8fa68 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -499,6 +499,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.0)
+ mini_magick (4.8.0)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.7.0)
@@ -1082,6 +1083,7 @@ DEPENDENCIES
loofah (~> 2.2)
mail_room (~> 0.9.1)
method_source (~> 0.8)
+ mini_magick
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.10)
diff --git a/INSTALLATION_TYPE b/INSTALLATION_TYPE
new file mode 100644
index 00000000000..5a18cd2fbf6
--- /dev/null
+++ b/INSTALLATION_TYPE
@@ -0,0 +1 @@
+source
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico
deleted file mode 100644
index 4af3582b60d..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico
deleted file mode 100644
index 13639da2e8a..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_created.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico
deleted file mode 100644
index 5f0e711b104..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico
deleted file mode 100644
index 8b1168a1267..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico
deleted file mode 100644
index ed19b69e1c5..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico
deleted file mode 100644
index 5dfefd4cc5a..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico
deleted file mode 100644
index a41539c0e3e..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_running.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico
deleted file mode 100644
index 2c1ae552b93..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico
deleted file mode 100644
index 70f0ca61eca..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_success.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico
deleted file mode 100644
index db289e03eb1..00000000000
--- a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico
deleted file mode 100644
index 23adcffff50..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_canceled.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.png b/app/assets/images/ci_favicons/favicon_status_canceled.png
new file mode 100644
index 00000000000..8adaa9c600b
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_canceled.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico
deleted file mode 100644
index f9d93b390d8..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_created.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_created.png b/app/assets/images/ci_favicons/favicon_status_created.png
new file mode 100644
index 00000000000..ca788dd0034
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_created.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico
deleted file mode 100644
index 28a22ebf724..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_failed.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_failed.png b/app/assets/images/ci_favicons/favicon_status_failed.png
new file mode 100644
index 00000000000..93f1e2772fd
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_failed.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico
deleted file mode 100644
index dbbf1abf30c..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_manual.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_manual.png b/app/assets/images/ci_favicons/favicon_status_manual.png
new file mode 100644
index 00000000000..c926062c806
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_manual.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico
deleted file mode 100644
index 49b9b232dd1..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_not_found.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.png b/app/assets/images/ci_favicons/favicon_status_not_found.png
new file mode 100644
index 00000000000..df3049315a9
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_not_found.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico
deleted file mode 100644
index 05962f3f148..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_pending.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_pending.png b/app/assets/images/ci_favicons/favicon_status_pending.png
new file mode 100644
index 00000000000..f7d67d4a230
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_pending.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico
deleted file mode 100644
index 7fa3d4d48d4..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_running.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_running.png b/app/assets/images/ci_favicons/favicon_status_running.png
new file mode 100644
index 00000000000..ff4167c4b20
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_running.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico
deleted file mode 100644
index b0c26b62068..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_skipped.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.png b/app/assets/images/ci_favicons/favicon_status_skipped.png
new file mode 100644
index 00000000000..a9c36464b69
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_skipped.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico
deleted file mode 100644
index b150960b5be..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_success.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_success.png b/app/assets/images/ci_favicons/favicon_status_success.png
new file mode 100644
index 00000000000..bcc30c73f5f
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_success.png
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico
deleted file mode 100644
index 7e71d71684d..00000000000
--- a/app/assets/images/ci_favicons/favicon_status_warning.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_warning.png b/app/assets/images/ci_favicons/favicon_status_warning.png
new file mode 100644
index 00000000000..6db3b0280f5
--- /dev/null
+++ b/app/assets/images/ci_favicons/favicon_status_warning.png
Binary files differ
diff --git a/app/assets/images/favicon-blue.png b/app/assets/images/favicon-blue.png
new file mode 100644
index 00000000000..2229fe79462
--- /dev/null
+++ b/app/assets/images/favicon-blue.png
Binary files differ
diff --git a/app/assets/images/favicon-yellow.ico b/app/assets/images/favicon-yellow.ico
deleted file mode 100644
index b650f277fb6..00000000000
--- a/app/assets/images/favicon-yellow.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/favicon-yellow.png b/app/assets/images/favicon-yellow.png
new file mode 100644
index 00000000000..2d5289818b4
--- /dev/null
+++ b/app/assets/images/favicon-yellow.png
Binary files differ
diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico
deleted file mode 100644
index 3479cbbb46f..00000000000
--- a/app/assets/images/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png
new file mode 100644
index 00000000000..845e0ec34a5
--- /dev/null
+++ b/app/assets/images/favicon.png
Binary files differ
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 84a7f277227..0692c96e767 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -87,10 +87,46 @@ export default {
mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: true,
- group: 'issues',
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id',
+ group: {
+ name: 'issues',
+ /**
+ * Dynamically determine between which containers
+ * items can be moved or copied as
+ * Assignee lists (EE feature) require this behavior
+ */
+ pull: (to, from, dragEl, e) => {
+ // As per Sortable's docs, `to` should provide
+ // reference to exact sortable container on which
+ // we're trying to drag element, but either it is
+ // a library's bug or our markup structure is too complex
+ // that `to` never points to correct container
+ // See https://github.com/RubaXa/Sortable/issues/1037
+ //
+ // So we use `e.target` which is always accurate about
+ // which element we're currently dragging our card upon
+ // So from there, we can get reference to actual container
+ // and thus the container type to enable Copy or Move
+ if (e.target) {
+ const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
+ const toBoardType = containerEl.dataset.boardType;
+
+ if (toBoardType) {
+ const fromBoardType = this.list.type;
+
+ if ((fromBoardType === 'assignee' && toBoardType === 'label') ||
+ (fromBoardType === 'label' && toBoardType === 'assignee')) {
+ return 'clone';
+ }
+ }
+ }
+
+ return true;
+ },
+ revertClone: true,
+ },
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
@@ -179,10 +215,11 @@ export default {
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
- class="board-list"
+ class="board-list js-board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
+ :data-board-type="list.type"
:class="{ 'is-smaller': showIssueForm }">
<board-card
v-for="(issue, index) in issues"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index e8dfd95f7ae..297c9eff38c 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -49,11 +49,12 @@ export default {
this.error = false;
const labels = this.list.label ? [this.list.label] : [];
+ const assignees = this.list.assignee ? [this.list.assignee] : [];
const issue = new ListIssue({
title: this.title,
labels,
subscribed: true,
- assignees: [],
+ assignees,
project_id: this.selectedProject.id,
});
@@ -141,4 +142,3 @@ export default {
</div>
</div>
</template>
-
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 71f49319c36..6dcd4aaec43 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -56,6 +56,7 @@ gl.issueBoards.newListDropdownInit = () => {
filterable: true,
selectable: true,
multiSelect: true,
+ containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
clicked (options) {
const { e } = options;
const label = options.selectedObj;
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 29ab13b8e0b..cdad8d238e3 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -7,6 +7,7 @@ import Vue from 'vue';
import Flash from '~/flash';
import { __ } from '~/locale';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
@@ -15,7 +16,6 @@ import './models/issue';
import './models/list';
import './models/milestone';
import './models/project';
-import './models/assignee';
import './stores/boards_store';
import ModalStore from './stores/modal_store';
import BoardService from './services/board_service';
diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js
deleted file mode 100644
index 05dd449e4fd..00000000000
--- a/app/assets/javascripts/boards/models/assignee.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-class ListAssignee {
- constructor(user, defaultAvatar) {
- this.id = user.id;
- this.name = user.name;
- this.username = user.username;
- this.avatar = user.avatar_url || defaultAvatar;
- }
-}
-
-window.ListAssignee = ListAssignee;
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 7144f4190e7..a79dd62e2e4 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,12 +1,14 @@
/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
/* global ListIssue */
-/* global ListLabel */
+
+import ListLabel from '~/vue_shared/models/label';
+import ListAssignee from '~/vue_shared/models/assignee';
import queryData from '../utils/query_data';
const PER_PAGE = 20;
class List {
- constructor (obj, defaultAvatar) {
+ constructor(obj, defaultAvatar) {
this.id = obj.id;
this._uid = this.guid();
this.position = obj.position;
@@ -24,6 +26,9 @@ class List {
if (obj.label) {
this.label = new ListLabel(obj.label);
+ } else if (obj.user) {
+ this.assignee = new ListAssignee(obj.user);
+ this.title = this.assignee.name;
}
if (this.type !== 'blank' && this.id) {
@@ -34,14 +39,25 @@ class List {
}
guid() {
- const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+ const s4 = () =>
+ Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
}
- save () {
+ save() {
+ const entity = this.label || this.assignee;
+ let entityType = '';
+ if (this.label) {
+ entityType = 'label_id';
+ } else {
+ entityType = 'assignee_id';
+ }
+
return gl.boardService.createList(this.label.id)
.then(res => res.data)
- .then((data) => {
+ .then(data => {
this.id = data.id;
this.type = data.list_type;
this.position = data.position;
@@ -50,25 +66,23 @@ class List {
});
}
- destroy () {
+ destroy() {
const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
- gl.boardService.destroyList(this.id)
- .catch(() => {
- // TODO: handle request error
- });
+ gl.boardService.destroyList(this.id).catch(() => {
+ // TODO: handle request error
+ });
}
- update () {
- gl.boardService.updateList(this.id, this.position)
- .catch(() => {
- // TODO: handle request error
- });
+ update() {
+ gl.boardService.updateList(this.id, this.position).catch(() => {
+ // TODO: handle request error
+ });
}
- nextPage () {
+ nextPage() {
if (this.issuesSize > this.issues.length) {
if (this.issues.length / PER_PAGE >= 1) {
this.page += 1;
@@ -78,7 +92,7 @@ class List {
}
}
- getIssues (emptyIssues = true) {
+ getIssues(emptyIssues = true) {
const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
if (this.label && data.label_name) {
@@ -89,7 +103,8 @@ class List {
this.loading = true;
}
- return gl.boardService.getIssuesForList(this.id, data)
+ return gl.boardService
+ .getIssuesForList(this.id, data)
.then(res => res.data)
.then((data) => {
this.loading = false;
@@ -103,11 +118,12 @@ class List {
});
}
- newIssue (issue) {
+ newIssue(issue) {
this.addIssue(issue, null, 0);
this.issuesSize += 1;
- return gl.boardService.newIssue(this.id, issue)
+ return gl.boardService
+ .newIssue(this.id, issue)
.then(res => res.data)
.then((data) => {
issue.id = data.id;
@@ -123,13 +139,13 @@ class List {
});
}
- createIssues (data) {
- data.forEach((issueObj) => {
+ createIssues(data) {
+ data.forEach(issueObj => {
this.addIssue(new ListIssue(issueObj, this.defaultAvatar));
});
}
- addIssue (issue, listFrom, newIndex) {
+ addIssue(issue, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
@@ -152,6 +168,13 @@ class List {
issue.addLabel(this.label);
}
+ if (this.assignee) {
+ if (listFrom && listFrom.type === 'assignee') {
+ issue.removeAssignee(listFrom.assignee);
+ }
+ issue.addAssignee(this.assignee);
+ }
+
if (listFrom) {
this.issuesSize += 1;
@@ -160,29 +183,29 @@ class List {
}
}
- moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
+ moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issue);
- gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId)
- .catch(() => {
- // TODO: handle request error
- });
+ gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => {
+ // TODO: handle request error
+ });
}
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
+ gl.boardService
+ .moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
.catch(() => {
// TODO: handle request error
});
}
- findIssue (id) {
+ findIssue(id) {
return this.issues.find(issue => issue.id === id);
}
- removeIssue (removeIssue) {
- this.issues = this.issues.filter((issue) => {
+ removeIssue(removeIssue) {
+ this.issues = this.issues.filter(issue => {
const matchesRemove = removeIssue.id === issue.id;
if (matchesRemove) {
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 7c90597f77c..029b0971f2c 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -30,11 +30,13 @@ export default class BoardService {
return axios.post(this.listsEndpointGenerate, {});
}
- createList(labelId) {
+ createList(entityId, entityType) {
+ const list = {
+ [entityType]: entityId,
+ };
+
return axios.post(this.listsEndpoint, {
- list: {
- label_id: labelId,
- },
+ list,
});
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 20e78edf2a2..7dc83843e9b 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -103,8 +103,15 @@ gl.issueBoards.BoardsStore = {
const listLabels = issueLists.map(listIssue => listIssue.label);
if (!issueTo) {
- // Add to new lists issues if it doesn't already exist
- listTo.addIssue(issue, listFrom, newIndex);
+ // Check if target list assignee is already present in this issue
+ if ((listTo.type === 'assignee' && listFrom.type === 'assignee') &&
+ issue.findAssignee(listTo.assignee)) {
+ const targetIssue = listTo.findIssue(issue.id);
+ targetIssue.removeAssignee(listFrom.assignee);
+ } else {
+ // Add to new lists issues if it doesn't already exist
+ listTo.addIssue(issue, listFrom, newIndex);
+ }
} else {
listTo.updateIssueLabel(issue, listFrom);
issueTo.removeLabel(listFrom.label);
@@ -115,7 +122,11 @@ gl.issueBoards.BoardsStore = {
list.removeIssue(issue);
});
issue.removeLabels(listLabels);
- } else {
+ } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
+ issue.removeAssignee(listFrom.assignee);
+ listFrom.removeIssue(issue);
+ } else if ((listTo.type !== 'label' && listFrom.type === 'assignee') ||
+ (listTo.type !== 'assignee' && listFrom.type === 'label')) {
listFrom.removeIssue(issue);
}
},
@@ -126,11 +137,12 @@ gl.issueBoards.BoardsStore = {
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
},
findList (key, val, type = 'label') {
- return this.state.lists.filter((list) => {
- const byType = type ? list['type'] === type : true;
+ const filteredList = this.state.lists.filter((list) => {
+ const byType = type ? (list.type === type) || (list.type === 'assignee') : true;
return list[key] === val && byType;
- })[0];
+ });
+ return filteredList[0];
},
updateFiltersUrl () {
history.pushState(null, null, `?${this.filter.path}`);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 746a06b7c4f..7fbba7e27cb 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -602,7 +602,11 @@ GitLabDropdown = (function() {
var selector;
selector = '.dropdown-content';
if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content";
+ if (this.options.containerSelector) {
+ selector = this.options.containerSelector;
+ } else {
+ selector = '.dropdown-page-one .dropdown-content';
+ }
}
return $(selector, this.dropdown).empty();
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index 5648cb9a888..d33e3a37580 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,7 +1,12 @@
import $ from 'jquery';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
-import { __ } from './locale';
+
+const tooltipTitles = {
+ group: __('Unsubscribe at group level'),
+ project: __('Unsubscribe at project level'),
+};
export default class GroupLabelSubscription {
constructor(container) {
@@ -35,6 +40,7 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.attr('data-url', url);
axios.post(url)
+ .then(() => GroupLabelSubscription.setNewTooltip($btn))
.then(() => this.toggleSubscriptionButtons())
.catch(() => flash(__('There was an error when subscribing to this label.')));
}
@@ -44,4 +50,14 @@ export default class GroupLabelSubscription {
this.$subscribeButtons.toggleClass('hidden');
this.$unsubscribeButtons.toggleClass('hidden');
}
+
+ static setNewTooltip($button) {
+ if (!$button.hasClass('js-subscribe-button')) return;
+
+ const type = $button.hasClass('js-group-level') ? 'group' : 'project';
+ const newTitle = tooltipTitles[type];
+
+ $('.js-unsubscribe-button', $button.closest('.label-actions-list'))
+ .tooltip('hide').attr('title', newTitle).tooltip('_fixTitle');
+ }
}
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 8c3de6e4045..8b01024b7d4 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -13,6 +13,7 @@ export default class LabelManager {
this.otherLabels = otherLabels || $('.js-other-labels');
this.errorMessage = 'Unable to update label prioritization at this time';
this.emptyState = document.querySelector('#js-priority-labels-empty-state');
+ this.$badgeItemTemplate = $('#js-badge-item-template');
this.sortable = Sortable.create(this.prioritizedLabels.get(0), {
filter: '.empty-message',
forceFallback: true,
@@ -63,7 +64,11 @@ export default class LabelManager {
$target = this.otherLabels;
$from = this.prioritizedLabels;
}
- $label.detach().appendTo($target);
+
+ const $detachedLabel = $label.detach();
+ this.toggleLabelPriorityBadge($detachedLabel, action);
+ $detachedLabel.appendTo($target);
+
if ($from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
@@ -88,6 +93,14 @@ export default class LabelManager {
}
}
+ toggleLabelPriorityBadge($label, action) {
+ if (action === 'remove') {
+ $('.js-priority-badge', $label).remove();
+ } else {
+ $('.label-links', $label).append(this.$badgeItemTemplate.clone().html());
+ }
+ }
+
onPrioritySortUpdate() {
this.savePrioritySort()
.catch(() => flash(this.errorMessage));
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8b5445d012b..d55d0585031 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -384,6 +384,49 @@ export const backOff = (fn, timeout = 60000) => {
});
};
+export const createOverlayIcon = (iconPath, overlayPath) => {
+ const faviconImage = document.createElement('img');
+
+ return new Promise((resolve) => {
+ faviconImage.onload = () => {
+ const size = 32;
+
+ const canvas = document.createElement('canvas');
+ canvas.width = size;
+ canvas.height = size;
+
+ const context = canvas.getContext('2d');
+ context.clearRect(0, 0, size, size);
+ context.drawImage(
+ faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size,
+ );
+
+ const overlayImage = document.createElement('img');
+ overlayImage.onload = () => {
+ context.drawImage(
+ overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size,
+ );
+
+ const faviconWithOverlayUrl = canvas.toDataURL();
+
+ resolve(faviconWithOverlayUrl);
+ };
+ overlayImage.src = overlayPath;
+ };
+ faviconImage.src = iconPath;
+ });
+};
+
+export const setFaviconOverlay = (overlayPath) => {
+ const faviconEl = document.getElementById('favicon');
+
+ if (!faviconEl) { return null; }
+
+ const iconPath = faviconEl.getAttribute('data-original-href');
+
+ return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl));
+};
+
export const setFavicon = (faviconPath) => {
const faviconEl = document.getElementById('favicon');
if (faviconEl && faviconPath) {
@@ -393,8 +436,9 @@ export const setFavicon = (faviconPath) => {
export const resetFavicon = () => {
const faviconEl = document.getElementById('favicon');
- const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
+
if (faviconEl) {
+ const originalFavicon = faviconEl.getAttribute('data-original-href');
faviconEl.setAttribute('href', originalFavicon);
}
};
@@ -403,10 +447,9 @@ export const setCiStatusFavicon = pageUrl =>
axios.get(pageUrl)
.then(({ data }) => {
if (data && data.favicon) {
- setFavicon(data.favicon);
- } else {
- resetFavicon();
+ return setFaviconOverlay(data.favicon);
}
+ return resetFavicon();
})
.catch(resetFavicon);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 0ff23bbb061..7cca32dc6fa 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -79,37 +79,37 @@ export function getTimeago() {
if (!timeagoInstance) {
const localeRemaining = function getLocaleRemaining(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
- [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')],
- [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')],
+ [s__('Timeago|just now'), s__('Timeago|right now')],
+ [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
+ [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
[s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
- [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')],
- [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')],
- [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')],
+ [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
+ [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
+ [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
[s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')],
- [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')],
+ [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')],
[s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')],
- [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')],
+ [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')],
[s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')],
- [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')],
+ [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
[s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
][index];
};
const locale = function getLocale(number, index) {
return [
- [s__('Timeago|less than a minute ago'), s__('Timeago|right now')],
- [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')],
- [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')],
+ [s__('Timeago|just now'), s__('Timeago|right now')],
+ [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
+ [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
[s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
- [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')],
- [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')],
- [s__('Timeago|a day ago'), s__('Timeago|in 1 day')],
+ [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
+ [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
+ [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
[s__('Timeago|%s days ago'), s__('Timeago|in %s days')],
- [s__('Timeago|a week ago'), s__('Timeago|in 1 week')],
+ [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')],
[s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')],
- [s__('Timeago|a month ago'), s__('Timeago|in 1 month')],
+ [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')],
[s__('Timeago|%s months ago'), s__('Timeago|in %s months')],
- [s__('Timeago|a year ago'), s__('Timeago|in 1 year')],
+ [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
[s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
][index];
};
@@ -270,6 +270,17 @@ export const totalDaysInMonth = date => {
};
/**
+ * Returns number of days in a quarter from provided
+ * months array.
+ *
+ * @param {Array} quarter
+ */
+export const totalDaysInQuarter = quarter => quarter.reduce(
+ (acc, month) => acc + totalDaysInMonth(month),
+ 0,
+);
+
+/**
* Returns list of Dates referring to Sundays of the month
* based on provided date
*
@@ -309,42 +320,27 @@ export const getSundays = date => {
};
/**
- * Returns list of Dates representing a timeframe of Months from month of provided date (inclusive)
- * up to provided length
- *
- * For eg;
- * If current month is January 2018 and `length` provided is `6`
- * Then this method will return list of Date objects as follows;
- *
- * [ October 2017, November 2017, December 2017, January 2018, February 2018, March 2018 ]
- *
- * If current month is March 2018 and `length` provided is `3`
- * Then this method will return list of Date objects as follows;
- *
- * [ February 2018, March 2018, April 2018 ]
+ * Returns list of Dates representing a timeframe of months from startDate and length
*
+ * @param {Date} startDate
* @param {Number} length
- * @param {Date} date
*/
-export const getTimeframeWindow = (length, date) => {
- if (!length) {
+export const getTimeframeWindowFrom = (startDate, length) => {
+ if (!(startDate instanceof Date) || !length) {
return [];
}
- const currentDate = date instanceof Date ? date : new Date();
- const currentMonthIndex = Math.floor(length / 2);
- const timeframe = [];
-
- // Move date object backward to the first month of timeframe
- currentDate.setDate(1);
- currentDate.setMonth(currentDate.getMonth() - currentMonthIndex);
-
- // Iterate and update date for the size of length
+ // Iterate and set date for the size of length
// and push date reference to timeframe list
- for (let i = 0; i < length; i += 1) {
- timeframe.push(new Date(currentDate.getTime()));
- currentDate.setMonth(currentDate.getMonth() + 1);
- }
+ const timeframe = new Array(length)
+ .fill()
+ .map(
+ (val, i) => new Date(
+ startDate.getFullYear(),
+ startDate.getMonth() + i,
+ 1,
+ ),
+ );
// Change date of last timeframe item to last date of the month
timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1]));
@@ -352,6 +348,29 @@ export const getTimeframeWindow = (length, date) => {
return timeframe;
};
+/**
+ * Returns count of day within current quarter from provided date
+ * and array of months for the quarter
+ *
+ * Eg;
+ * If date is 15 Feb 2018
+ * and quarter is [Jan, Feb, Mar]
+ *
+ * Then 15th Feb is 46th day of the quarter
+ * Where 31 (days in Jan) + 15 (date of Feb).
+ *
+ * @param {Date} date
+ * @param {Array} quarter
+ */
+export const dayInQuarter = (date, quarter) => quarter.reduce((acc, month) => {
+ if (date.getMonth() > month.getMonth()) {
+ return acc + totalDaysInMonth(month);
+ } else if (date.getMonth() === month.getMonth()) {
+ return acc + date.getDate();
+ }
+ return acc + 0;
+}, 0);
+
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index dd17544b656..72b72f4247d 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -85,9 +85,9 @@ export function redirectTo(url) {
}
export function webIDEUrl(route = undefined) {
- let returnUrl = `${gon.relative_url_root}/-/ide/`;
+ let returnUrl = `${gon.relative_url_root || ''}/-/ide/`;
if (route) {
- returnUrl += `project${route}`;
+ returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`;
}
return returnUrl;
}
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 6c2a785c0af..37ef77c8e43 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -22,4 +22,18 @@ document.addEventListener('DOMContentLoaded', () => {
errorBox: variableListEl.querySelector('.js-ci-variable-error-box'),
saveEndpoint: variableListEl.dataset.saveEndpoint,
});
+
+ // hide extra auto devops settings based on data-attributes
+ const autoDevOpsSettings = document.querySelector('.js-auto-devops-settings');
+ const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings');
+
+ autoDevOpsSettings.addEventListener('click', event => {
+ const target = event.target;
+ if (target.classList.contains('js-toggle-extra-settings')) {
+ autoDevOpsExtraSettings.classList.toggle(
+ 'hidden',
+ !!(target.dataset && target.dataset.hideExtraSettings),
+ );
+ }
+ });
});
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index 6f06944ebb6..9049f87e037 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -3,6 +3,17 @@ import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
+const tooltipTitles = {
+ group: {
+ subscribed: __('Unsubscribe at group level'),
+ unsubscribed: __('Subscribe at group level'),
+ },
+ project: {
+ subscribed: __('Unsubscribe at project level'),
+ unsubscribed: __('Subscribe at project level'),
+ },
+};
+
export default class ProjectLabelSubscription {
constructor(container) {
this.$container = $(container);
@@ -15,12 +26,10 @@ export default class ProjectLabelSubscription {
event.preventDefault();
const $btn = $(event.currentTarget);
- const $span = $btn.find('span');
const url = $btn.attr('data-url');
const oldStatus = $btn.attr('data-status');
$btn.addClass('disabled');
- $span.toggleClass('hidden');
axios.post(url).then(() => {
let newStatus;
@@ -32,21 +41,28 @@ export default class ProjectLabelSubscription {
[newStatus, newAction] = ['unsubscribed', 'Subscribe'];
}
- $span.toggleClass('hidden');
$btn.removeClass('disabled');
this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction);
- this.$buttons.map((button) => {
+ this.$buttons.map((i, button) => {
const $button = $(button);
+ const originalTitle = $button.attr('data-original-title');
- if ($button.attr('data-original-title')) {
- $button.tooltip('hide').attr('data-original-title', newAction).tooltip('_fixTitle');
+ if (originalTitle) {
+ ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction);
}
return button;
});
}).catch(() => flash(__('There was an error subscribing to this label.')));
}
+
+ static setNewTitle($button, originalTitle, newStatus) {
+ const type = /group/.test(originalTitle) ? 'group' : 'project';
+ const newTitle = tooltipTitles[type][newStatus];
+
+ $button.attr('title', newTitle).tooltip('_fixTitle');
+ }
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index c20d07a169d..098e8178265 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -36,7 +36,7 @@ import {
notify,
SourceBranchRemovalStatus,
} from './dependencies';
-import { setFavicon } from '../lib/utils/common_utils';
+import { setFaviconOverlay } from '../lib/utils/common_utils';
export default {
el: '#js-vue-mr-widget',
@@ -159,8 +159,9 @@ export default {
},
setFaviconHelper() {
if (this.mr.ciStatusFaviconPath) {
- setFavicon(this.mr.ciStatusFaviconPath);
+ return setFaviconOverlay(this.mr.ciStatusFaviconPath);
}
+ return Promise.resolve();
},
fetchDeployments() {
return this.service.fetchDeployments()
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index 9ffbaae3ea5..9bca1993ccc 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -513,7 +513,7 @@ const fileNameIcons = {
'credits.md': 'credits',
'credits.md.rendered': 'credits',
'.flowconfig': 'flow',
- 'favicon.ico': 'favicon',
+ 'favicon.png': 'favicon',
'karma.conf.js': 'karma',
'karma.conf.ts': 'karma',
'karma.conf.coffee': 'karma',
diff --git a/app/assets/javascripts/vue_shared/models/assignee.js b/app/assets/javascripts/vue_shared/models/assignee.js
new file mode 100644
index 00000000000..4a29b0d0581
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/models/assignee.js
@@ -0,0 +1,13 @@
+export default class ListAssignee {
+ constructor(obj, defaultAvatar) {
+ this.id = obj.id;
+ this.name = obj.name;
+ this.username = obj.username;
+ this.avatar = obj.avatar_url || obj.avatar || defaultAvatar;
+ this.path = obj.path;
+ this.state = obj.state;
+ this.webUrl = obj.web_url || obj.webUrl;
+ }
+}
+
+window.ListAssignee = ListAssignee;
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 79f580546c3..79e59fb5263 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -233,6 +233,13 @@ table {
}
}
+.card-header {
+ h3.card-title,
+ h4.card-title {
+ margin-top: 0;
+ }
+}
+
.nav-tabs {
// Override bootstrap's default border
border-bottom: 0;
@@ -261,3 +268,7 @@ pre code {
color: $white-light;
}
}
+
+input[type=color].form-control {
+ height: $input-height;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 0115f542c88..88b174491dd 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -497,6 +497,10 @@ fieldset[disabled] .btn,
}
}
+[readonly] {
+ cursor: default;
+}
+
.btn-no-padding {
padding: 0;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index f2ac77819d5..6fbc624dee4 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -139,6 +139,8 @@
}
.nav {
+ flex-wrap: nowrap;
+
> li:not(.d-none) a {
@include media-breakpoint-down(xs) {
margin-left: 0;
@@ -158,11 +160,12 @@
}
.navbar-toggler {
+ position: relative;
right: -10px;
border-radius: 0;
min-width: 45px;
padding: 0;
- margin-right: -7px;
+ margin: $gl-padding-8 -7px $gl-padding-8 0;
font-size: 14px;
text-align: center;
color: currentColor;
@@ -186,6 +189,7 @@
display: -webkit-flex;
display: flex;
padding-right: 10px;
+ flex-direction: row;
}
li {
@@ -290,6 +294,10 @@
margin: 8px;
}
}
+
+ .dropdown-menu {
+ position: absolute;
+ }
}
.navbar-sub-nav {
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 10c23f6c407..6e1758d7677 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -39,6 +39,11 @@ table {
&.wide {
width: 55%;
}
+
+ &.table-th-transparent {
+ background: none;
+ color: $gl-text-color-secondary;
+ }
}
td {
@@ -46,9 +51,86 @@ table {
}
}
}
+
+ &.responsive-table {
+ @include media-breakpoint-down(sm) {
+ thead {
+ display: none;
+ }
+
+ td {
+ display: block;
+ color: $gl-text-color-secondary;
+ }
+
+ tbody td.responsive-table-cell {
+ padding: $gl-padding 0;
+ width: 100%;
+ display: flex;
+ text-align: right;
+ align-items: center;
+ justify-content: space-between;
+
+ &[data-column]::before {
+ content: attr(data-column);
+ display: block;
+ text-align: left;
+ padding-right: $gl-padding;
+ color: $gl-text-color-secondary;
+ }
+
+ &:not([data-column]) {
+ flex-direction: row-reverse;
+ }
+ }
+
+ tr.responsive-table-border-start,
+ tr.responsive-table-border-end {
+ display: block;
+ border: solid $gl-text-color-quaternary;
+ padding-left: 0;
+ padding-right: 0;
+
+ > td {
+ border-color: $gl-text-color-quaternary;
+
+ &,
+ &:last-child {
+ padding-left: $gl-padding;
+ padding-right: $gl-padding;
+ }
+ }
+ }
+
+ tr.responsive-table-border-start {
+ border-width: 1px 1px 0;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ padding-top: 0;
+ padding-bottom: 0;
+
+ > td:first-child {
+ border-top: 0; // always have the <table> top border
+ }
+
+ > td:last-child {
+ border-bottom: 1px solid $gl-text-color-quaternary;
+ }
+ }
+
+ tr.responsive-table-border-end {
+ border-width: 0 1px 1px;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
+ margin-bottom: 2 * $gl-padding;
+
+ > :last-child {
+ border-bottom: 0;
+ }
+ }
+ }
+ }
}
-.responsive-table {
+.responsive-table:not(table) {
@include media-breakpoint-down(sm) {
th {
width: 100%;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index e6e74d55f64..497261f938f 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -174,11 +174,6 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
$border-gray-dark: darken($white-normal, $darken-border-factor);
/*
- * Override Bootstrap 4 variables
- */
-$secondary: $gray-light;
-
-/*
* UI elements
*/
$border-color: #e5e5e5;
@@ -808,3 +803,16 @@ $modal-body-height: 134px;
Prometheus
*/
$prometheus-table-row-highlight-color: $theme-gray-100;
+
+$priority-label-empty-state-width: 114px;
+
+/*
+ * Override Bootstrap 4 variables
+ */
+
+$secondary: $gray-light;
+$input-disabled-bg: $gray-light;
+$input-border-color: $theme-gray-200;
+$input-color: $gl-text-color;
+$font-family-sans-serif: $regular_font;
+$font-family-monospace: $monospace_font;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 9213ccd4cdf..f030189af06 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -12,26 +12,22 @@
@keyframes blinking-dots {
0% {
background-color: rgba($white-light, 1);
- box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2);
}
25% {
background-color: rgba($white-light, 0.4);
- box-shadow: 12px 0 0 0 rgba($white-light, 2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ box-shadow: 12px 0 0 0 rgba($white-light, 2), 24px 0 0 0 rgba($white-light, 0.2);
}
75% {
background-color: rgba($white-light, 0.4);
- box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 1);
+ box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 1);
}
100% {
background-color: rgba($white-light, 1);
- box-shadow: 12px 0 0 0 rgba($white-light, 0.2),
- 24px 0 0 0 rgba($white-light, 0.2);
+ box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2);
}
}
@@ -71,6 +67,10 @@
.bash {
display: block;
}
+
+ &.build-trace-rounded {
+ border-radius: $border-radius-base;
+ }
}
.top-bar {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index e178371d21f..785df23a355 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -57,69 +57,8 @@
border-bottom-left-radius: $border-radius-base;
}
-.label-row {
- .label-name {
- display: inline-block;
- margin-bottom: 10px;
-
- @include media-breakpoint-up(sm) {
- width: 200px;
- margin-left: $gl-padding * 2;
- margin-bottom: 0;
- }
-
- .badge {
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 100%;
- }
- }
-
- .label-type {
- display: block;
- margin-bottom: 10px;
- margin-left: 50px;
-
- @include media-breakpoint-up(sm) {
- display: inline-block;
- width: 100px;
- margin-left: 10px;
- margin-bottom: 0;
- vertical-align: top;
- }
- }
-
- .label-description {
- display: block;
- margin-bottom: 10px;
-
- .description-text {
- margin-bottom: $gl-padding;
- }
-
- a {
- color: $blue-600;
- }
-
- @include media-breakpoint-up(sm) {
- display: inline-block;
- max-width: 50%;
- margin-left: 10px;
- margin-bottom: 0;
- vertical-align: top;
- }
- }
-
- .badge {
- padding: 4px $grid-size;
- font-size: $label-font-size;
- position: relative;
- top: ($grid-size / 2);
- }
-}
-
.color-label {
- padding: 0 $grid-size;
+ padding: $gl-padding-4 $grid-size;
line-height: 16px;
border-radius: $label-border-radius;
color: $white-light;
@@ -133,26 +72,29 @@
}
.manage-labels-list {
- @media(min-width: map-get($grid-breakpoints, md)) {
- &.content-list li {
- padding: $gl-padding 0;
- }
- }
-
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
- cursor: move;
- cursor: -webkit-grab;
- cursor: -moz-grab;
-
- &:active {
- cursor: -webkit-grabbing;
- cursor: -moz-grabbing;
- }
+ margin-bottom: 5px;
+ display: flex;
+ justify-content: space-between;
+ padding: $gl-padding;
+ border-radius: $border-radius-default;
&.sortable-ghost {
opacity: 0.3;
}
+
+ .prioritized-labels & {
+ box-shadow: 0 1px 2px $issue-boards-card-shadow;
+ cursor: move;
+ cursor: -webkit-grab;
+ cursor: -moz-grab;
+
+ &:active {
+ cursor: -webkit-grabbing;
+ cursor: -moz-grabbing;
+ }
+ }
}
.btn-action {
@@ -170,27 +112,6 @@
}
}
}
-
- .dropdown {
- @include media-breakpoint-up(sm) {
- float: right;
- }
- }
-
- @include media-breakpoint-down(xs) {
- .dropdown-menu {
- min-width: 100%;
- }
- }
-}
-
-.draggable-handler {
- display: inline-block;
- vertical-align: top;
- margin: 5px 0;
- opacity: 0;
- transition: opacity .3s;
- color: $gray-darkest;
}
.prioritized-labels {
@@ -215,22 +136,6 @@
}
}
-.toggle-priority {
- display: inline-block;
- vertical-align: top;
-
- button {
- border-color: transparent;
- padding: 5px 8px;
- vertical-align: top;
- font-size: 14px;
-
- &:hover {
- border-color: transparent;
- }
- }
-}
-
.filtered-labels {
font-size: 0;
padding: 12px 16px;
@@ -284,10 +189,8 @@
}
.label-subscribe-button {
- @media(min-width: map-get($grid-breakpoints, md)) {
- min-width: 105px;
- margin-left: $gl-padding;
- }
+ width: 105px;
+ font-weight: 200;
.label-subscribe-button-icon {
&[disabled] {
@@ -324,3 +227,95 @@
font-size: $label-font-size;
}
}
+
+.labels-container {
+ background-color: $gray-light;
+ border-radius: $border-radius-default;
+ padding: $gl-padding $gl-padding-8;
+}
+
+.label-actions-list {
+ list-style: none;
+ flex-shrink: 0;
+ padding: 0;
+}
+
+.label-badge {
+ color: $theme-gray-900;
+ font-weight: $gl-font-weight-normal;
+ padding: $gl-padding-4 $gl-padding-8;
+ border-radius: $border-radius-default;
+ font-size: $label-font-size;
+}
+
+.label-badge-blue {
+ background-color: $theme-blue-100;
+}
+
+.label-badge-gray {
+ background-color: $theme-gray-100;
+}
+
+.label-links {
+ list-style: none;
+ padding: 0;
+ white-space: nowrap;
+}
+
+.label-link-item {
+ padding: 0;
+}
+
+.label-list-item {
+ .content-list &::before,
+ .content-list &::after {
+ content: none;
+ }
+
+ .label-name {
+ width: 150px;
+ flex-shrink: 0;
+
+ .label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ }
+ }
+
+ .label-description {
+ flex-grow: 1;
+
+ a {
+ color: $blue-600;
+ }
+ }
+
+ .label {
+ padding: 4px $grid-size;
+ font-size: $label-font-size;
+ position: relative;
+ top: $gl-padding-4;
+ }
+
+ .label-action {
+ color: $theme-gray-800;
+ cursor: pointer;
+
+ svg {
+ fill: $theme-gray-800;
+ }
+
+ &:hover {
+ color: $blue-600;
+
+ svg {
+ fill: $blue-600;
+ }
+ }
+ }
+}
+
+.priority-labels-empty-state .svg-content img {
+ max-width: $priority-label-empty-state-width;
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index f85f66b9c0b..30428fd198d 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -321,18 +321,17 @@
}
.build-failures {
+ th {
+ border-top: 0;
+ }
+
.build-state {
padding: 20px 2px;
.build-name {
- float: right;
font-weight: $gl-font-weight-normal;
}
- .ci-status-icon-failed svg {
- vertical-align: middle;
- }
-
.stage {
color: $gl-text-color-secondary;
font-weight: $gl-font-weight-normal;
@@ -344,6 +343,81 @@
border: 0;
line-height: initial;
}
+
+ .build-trace-row td {
+ border-top: 0;
+ border-bottom-width: 1px;
+ border-bottom-style: solid;
+ padding-top: 0;
+ }
+
+ .build-trace {
+ width: 100%;
+ text-align: left;
+ margin-top: $gl-padding;
+ }
+
+ .build-name {
+ width: 196px;
+
+ a {
+ font-weight: $gl-font-weight-bold;
+ color: $gl-text-color;
+ text-decoration: none;
+
+ &:focus,
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .build-actions {
+ width: 70px;
+ text-align: right;
+ }
+
+ .build-stage {
+ width: 140px;
+ }
+
+ .ci-status-icon-failed {
+ padding: 10px 0 10px 12px;
+ width: 12px + 24px; // padding-left + svg width
+ }
+
+ .build-icon svg {
+ width: 24px;
+ height: 24px;
+ vertical-align: middle;
+ }
+
+ .build-state,
+ .build-trace-row {
+ > td:last-child {
+ padding-right: 0;
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ td:empty {
+ display: none;
+ }
+
+ .ci-table {
+ margin-top: 2 * $gl-padding;
+ }
+
+ .build-trace-container {
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
+ }
+
+ .build-trace {
+ margin-bottom: 0;
+ margin-top: 0;
+ }
+ }
}
.pipeline-tab-content {
@@ -929,7 +1003,7 @@ button.mini-pipeline-graph-dropdown-toggle {
&.dropdown-menu {
transform: translate(-80%, 0);
- @media(min-width: map-get($grid-breakpoints, md)) {
+ @media (min-width: map-get($grid-breakpoints, md)) {
transform: translate(-50%, 0);
right: auto;
left: 50%;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index a35c4ff7c80..5f15795a8e3 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -18,7 +18,8 @@
.file-finder-input:hover,
.issuable-search-form:hover,
.search-text-input:hover,
-.form-control:hover {
+.form-control:hover,
+:not[readonly] {
border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 16e999341da..1f8e61257a9 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -127,12 +127,16 @@
color: $gl-danger;
}
-.service-settings .form-control-label {
- padding-top: 0;
+.service-settings {
+ input[type="radio"],
+ input[type="checkbox"] {
+ margin-top: 10px;
+ }
}
.integration-settings-form {
- .card.card-body {
+ .card.card-body,
+ .info-well {
padding: $gl-padding / 2;
box-shadow: none;
}
diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss
index a355e2dee24..777fdb3581e 100644
--- a/app/assets/stylesheets/pages/settings_ci_cd.scss
+++ b/app/assets/stylesheets/pages/settings_ci_cd.scss
@@ -16,3 +16,12 @@
.registry-placeholder {
min-height: 60px;
}
+
+.auto-devops-card {
+ margin-bottom: $gl-vert-padding;
+
+ > .card-body {
+ border-radius: $card-border-radius;
+ padding: $gl-padding $gl-padding-24;
+ }
+}
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 06ef58531d7..8cdf2275551 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -15,6 +15,7 @@
color: $perf-bar-text;
select {
+ color: $perf-bar-text;
width: 200px;
}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index ea302f17d16..9aaec905734 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -41,6 +41,13 @@ class Admin::AppearancesController < Admin::ApplicationController
redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.'
end
+ def favicon
+ @appearance.remove_favicon!
+ @appearance.save
+
+ redirect_to admin_appearances_path, notice: 'Favicon was succesfully removed.'
+ end
+
private
# Use callbacks to share common setup or constraints between actions.
@@ -61,6 +68,8 @@ class Admin::AppearancesController < Admin::ApplicationController
logo_cache
header_logo
header_logo_cache
+ favicon
+ favicon_cache
new_project_guidelines
updated_by
]
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
index 381fd4d7508..e8b5934f2a9 100644
--- a/app/controllers/boards/lists_controller.rb
+++ b/app/controllers/boards/lists_controller.rb
@@ -56,8 +56,12 @@ module Boards
private
+ def list_creation_attrs
+ %i[label_id]
+ end
+
def list_params
- params.require(:list).permit(:label_id)
+ params.require(:list).permit(list_creation_attrs)
end
def move_params
@@ -65,11 +69,15 @@ module Boards
end
def serialize_as_json(resource)
- resource.as_json(
+ resource.as_json(serialization_attrs)
+ end
+
+ def serialization_attrs
+ {
only: [:id, :list_type, :position],
methods: [:title],
label: true
- )
+ }
end
end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index c925b4aada5..d04eb192129 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -7,6 +7,19 @@ module IssuableActions
before_action :authorize_admin_issuable!, only: :bulk_update
end
+ def permitted_keys
+ [
+ :issuable_ids,
+ :assignee_id,
+ :milestone_id,
+ :state_event,
+ :subscription_event,
+ label_ids: [],
+ add_label_ids: [],
+ remove_label_ids: []
+ ]
+ end
+
def show
respond_to do |format|
format.html
@@ -140,24 +153,15 @@ module IssuableActions
end
def bulk_update_params
- permitted_keys = [
- :issuable_ids,
- :assignee_id,
- :milestone_id,
- :state_event,
- :subscription_event,
- label_ids: [],
- add_label_ids: [],
- remove_label_ids: []
- ]
+ permitted_keys_array = permitted_keys.dup
if resource_name == 'issue'
- permitted_keys << { assignee_ids: [] }
+ permitted_keys_array << { assignee_ids: [] }
else
- permitted_keys.unshift(:assignee_id)
+ permitted_keys_array.unshift(:assignee_id)
end
- params.require(:update).permit(permitted_keys)
+ params.require(:update).permit(permitted_keys_array)
end
def resource_name
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index ca1b80a36a0..2ef2ee76855 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -95,12 +95,7 @@ module IssuableCollections
elsif @group
@filter_params[:group_id] = @group.id
@filter_params[:include_subgroups] = true
- else
- # TODO: this filter ignore issues/mr created in public or
- # internal repos where you are not a member. Enable this filter
- # or improve current implementation to filter only issues you
- # created or assigned or mentioned
- # @filter_params[:authorized_only] = true
+ @filter_params[:use_cte_for_search] = true
end
@filter_params.permit(finder_type.valid_params)
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index b9b9b6e4e88..170bca8b56f 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -2,7 +2,7 @@ module UploadsActions
include Gitlab::Utils::StrongMemoize
include SendFileUpload
- UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze
+ UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
@@ -31,6 +31,11 @@ module UploadsActions
disposition = uploader.image_or_video? ? 'inline' : 'attachment'
+ uploaders = [uploader, *uploader.versions.values]
+ uploader = uploaders.find { |version| version.filename == params[:filename] }
+
+ return render_404 unless uploader
+
send_upload(uploader, attachment: uploader.filename, disposition: disposition)
end
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 58be330f466..863f50e8e66 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -2,6 +2,7 @@ class Groups::LabelsController < Groups::ApplicationController
include ToggleSubscriptionAction
before_action :label, only: [:edit, :update, :destroy]
+ before_action :available_labels, only: [:index]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
@@ -12,17 +13,8 @@ class Groups::LabelsController < Groups::ApplicationController
format.html do
@labels = @group.labels.page(params[:page])
end
-
format.json do
- available_labels = LabelsFinder.new(
- current_user,
- group_id: @group.id,
- only_group_labels: params[:only_group_labels],
- include_ancestor_groups: params[:include_ancestor_groups],
- include_descendant_groups: params[:include_descendant_groups]
- ).execute
-
- render json: LabelSerializer.new.represent_appearance(available_labels)
+ render json: LabelSerializer.new.represent_appearance(@available_labels)
end
end
end
@@ -113,4 +105,15 @@ class Groups::LabelsController < Groups::ApplicationController
def save_previous_label_path
session[:previous_labels_path] = URI(request.referer || '').path
end
+
+ def available_labels
+ @available_labels ||=
+ LabelsFinder.new(
+ current_user,
+ group_id: @group.id,
+ only_group_labels: params[:only_group_labels],
+ include_ancestor_groups: params[:include_ancestor_groups],
+ include_descendant_groups: params[:include_descendant_groups]
+ ).execute
+ end
end
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
index 4d758402850..a5c82caa897 100644
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -42,6 +42,6 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
owner: current_user
}
- Applications::CreateService.new(current_user, oauth_application_params).execute
+ Applications::CreateService.new(current_user, oauth_application_params).execute(request)
end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 1d850baf012..fb3f6eec2bd 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -41,7 +41,7 @@ module Projects
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_human_readable, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
- auto_devops_attributes: [:id, :domain, :enabled]
+ auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy]
)
end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 067aff408df..2a656c0d31c 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -3,17 +3,29 @@ class GroupMembersFinder
@group = group
end
- def execute
+ def execute(include_descendants: false)
group_members = @group.members
+ wheres = []
- return group_members unless @group.parent
+ return group_members unless @group.parent || include_descendants
- parents_members = GroupMember.non_request
- .where(source_id: @group.ancestors.select(:id))
- .where.not(user_id: @group.users.select(:id))
+ wheres << "members.id IN (#{group_members.select(:id).to_sql})"
- wheres = ["members.id IN (#{group_members.select(:id).to_sql})"]
- wheres << "members.id IN (#{parents_members.select(:id).to_sql})"
+ if @group.parent
+ parents_members = GroupMember.non_request
+ .where(source_id: @group.ancestors.select(:id))
+ .where.not(user_id: @group.users.select(:id))
+
+ wheres << "members.id IN (#{parents_members.select(:id).to_sql})"
+ end
+
+ if include_descendants
+ descendant_members = GroupMember.non_request
+ .where(source_id: @group.descendants.select(:id))
+ .where.not(user_id: @group.users.select(:id))
+
+ wheres << "members.id IN (#{descendant_members.select(:id).to_sql})"
+ end
GroupMember.where(wheres.join(' OR '))
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index c6ef79ce15e..5d5f72c4d86 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -23,6 +23,7 @@
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
+# use_cte_for_search: boolean
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
@@ -54,6 +55,7 @@ class IssuableFinder
sort
state
include_subgroups
+ use_cte_for_search
]
end
@@ -74,19 +76,21 @@ class IssuableFinder
items = init_collection
items = filter_items(items)
- # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
- items = by_project(items)
+ # This has to be last as we may use a CTE as an optimization fence by
+ # passing the use_cte_for_search param
+ # https://www.postgresql.org/docs/current/static/queries-with.html
+ items = by_search(items)
sort(items)
end
def filter_items(items)
+ items = by_project(items)
items = by_scope(items)
items = by_created_at(items)
items = by_updated_at(items)
items = by_state(items)
items = by_group(items)
- items = by_search(items)
items = by_assignee(items)
items = by_author(items)
items = by_non_archived(items)
@@ -107,7 +111,6 @@ class IssuableFinder
#
def count_by_state
count_params = params.merge(state: nil, sort: nil)
- labels_count = label_names.any? ? label_names.count : 1
finder = self.class.new(current_user, count_params)
counts = Hash.new(0)
@@ -116,6 +119,11 @@ class IssuableFinder
# per issuable, so we have to count those in Ruby - which is bad, but still
# better than performing multiple queries.
#
+ # This does not apply when we are using a CTE for the search, as the labels
+ # GROUP BY is inside the subquery in that case, so we set labels_count to 1.
+ labels_count = label_names.any? ? label_names.count : 1
+ labels_count = 1 if use_cte_for_search?
+
finder.execute.reorder(nil).group(:state).count.each do |key, value|
counts[Array(key).last.to_sym] += value / labels_count
end
@@ -159,10 +167,7 @@ class IssuableFinder
finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute
else
- opts = { current_user: current_user }
- opts[:project_ids_relation] = item_project_ids(items) if items
-
- ProjectsFinder.new(opts).execute
+ ProjectsFinder.new(current_user: current_user).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
@@ -329,8 +334,24 @@ class IssuableFinder
items
end
+ def use_cte_for_search?
+ return false unless search
+ return false unless Gitlab::Database.postgresql?
+
+ params[:use_cte_for_search]
+ end
+
def by_search(items)
- search ? items.full_search(search) : items
+ return items unless search
+
+ if use_cte_for_search?
+ cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name)
+ cte << items
+
+ items = klass.with(cte.to_arel).from(klass.table_name)
+ end
+
+ items.full_search(search)
end
def by_iids(items)
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 3626670d141..24a6b9349a0 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -136,8 +136,4 @@ class IssuesFinder < IssuableFinder
items
end
end
-
- def item_project_ids(items)
- items&.reorder(nil)&.select(:project_id)
- end
end
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index 4734d97b8c7..4c893ae2de6 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -7,12 +7,12 @@ class MembersFinder
@group = project.group
end
- def execute
+ def execute(include_descendants: false)
project_members = project.project_members
project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
if group
- group_members = GroupMembersFinder.new(group).execute
+ group_members = GroupMembersFinder.new(group).execute(include_descendants: include_descendants)
group_members = group_members.non_invite
union = Gitlab::SQL::Union.new([project_members, group_members], remove_duplicates: false)
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index e2240e5e0d8..8d84ed4bdfb 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -56,8 +56,4 @@ class MergeRequestsFinder < IssuableFinder
items.where(target_branch: target_branch)
end
-
- def item_project_ids(items)
- items&.reorder(nil)&.select(:target_project_id)
- end
end
diff --git a/app/helpers/favicon_helper.rb b/app/helpers/favicon_helper.rb
new file mode 100644
index 00000000000..3a5342a8d9d
--- /dev/null
+++ b/app/helpers/favicon_helper.rb
@@ -0,0 +1,7 @@
+module FaviconHelper
+ def favicon_extension_whitelist
+ FaviconUploader::EXTENSION_WHITELIST
+ .map { |extension| "'.#{extension}'"}
+ .to_sentence
+ end
+end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index e1b0e7a4a3e..c7df25cecef 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -211,6 +211,14 @@ module LabelsHelper
end
end
+ def label_status_tooltip(label, status)
+ type = label.is_a?(ProjectLabel) ? 'project' : 'group'
+ level = status.unsubscribed? ? type : status.sub('-level', '')
+ action = status.unsubscribed? ? 'Subscribe' : 'Unsubscribe'
+
+ "#{action} at #{level} level"
+ end
+
# Required for Banzai::Filter::LabelReferenceFilter
module_function :render_colored_label, :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index a8397b03d63..68d892393ef 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -39,10 +39,7 @@ module PageLayoutHelper
end
def favicon
- return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY'])
- return 'favicon-blue.ico' if Rails.env.development?
-
- 'favicon.ico'
+ Gitlab::Favicon.main
end
def page_image
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 67cc84a9140..b770aadef0e 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -14,6 +14,7 @@ class Appearance < ActiveRecord::Base
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
+ mount_uploader :favicon, FaviconUploader
# Overrides CacheableAttributes.current_without_cache
def self.current_without_cache
diff --git a/app/models/group.rb b/app/models/group.rb
index 8fb77a7869d..9c171de7fc3 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -11,6 +11,7 @@ class Group < Namespace
include GroupDescendant
include TokenAuthenticatable
include WithUploads
+ include Gitlab::Utils::StrongMemoize
has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
alias_method :members, :group_members
@@ -26,7 +27,11 @@ class Group < Namespace
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project
+
+ # Overridden on another method
+ # Left here just to be dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
+
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
has_many :custom_attributes, class_name: 'GroupCustomAttribute'
@@ -88,6 +93,15 @@ class Group < Namespace
end
end
+ # Overrides notification_settings has_many association
+ # This allows to apply notification settings from parent groups
+ # to child groups and projects.
+ def notification_settings
+ source_type = self.class.base_class.name
+
+ NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids)
+ end
+
def to_reference(_from = nil, full: nil)
"#{self.class.reference_prefix}#{full_path}"
end
@@ -141,13 +155,14 @@ class Group < Namespace
)
end
- def add_user(user, access_level, current_user: nil, expires_at: nil)
+ def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false)
GroupMember.add_user(
self,
user,
access_level,
current_user: current_user,
- expires_at: expires_at
+ expires_at: expires_at,
+ ldap: ldap
)
end
@@ -195,6 +210,10 @@ class Group < Namespace
owners.include?(user) && owners.size == 1
end
+ def ldap_synced?
+ false
+ end
+
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
@@ -220,6 +239,12 @@ class Group < Namespace
members_with_parents.pluck(:user_id)
end
+ def self_and_ancestors_ids
+ strong_memoize(:self_and_ancestors_ids) do
+ self_and_ancestors.pluck(:id)
+ end
+ end
+
def members_with_parents
# Avoids an unnecessary SELECT when the group has no parents
source_ids =
diff --git a/app/models/label.rb b/app/models/label.rb
index de7f1d56c64..1cf04976602 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -137,6 +137,10 @@ class Label < ActiveRecord::Base
priority.try(:priority)
end
+ def priority?
+ priorities.present?
+ end
+
def template?
template
end
diff --git a/app/models/list.rb b/app/models/list.rb
index 5daf35ef845..4edcfa78835 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -2,17 +2,27 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
- enum list_type: { backlog: 0, label: 1, closed: 2 }
+ enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
validates :label_id, uniqueness: { scope: :board_id }, if: :label?
- validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :label?
+ validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable?
before_destroy :can_be_destroyed
- scope :destroyable, -> { where(list_type: list_types[:label]) }
- scope :movable, -> { where(list_type: list_types[:label]) }
+ scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) }
+ scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) }
+
+ class << self
+ def destroyable_types
+ [:label]
+ end
+
+ def movable_types
+ [:label]
+ end
+ end
def destroyable?
label?
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 2c3580bbdc6..1a03dd9df56 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -1,4 +1,6 @@
class NotificationRecipient
+ include Gitlab::Utils::StrongMemoize
+
attr_reader :user, :type, :reason
def initialize(user, type, **opts)
unless NotificationSetting.levels.key?(type) || type == :subscription
@@ -64,7 +66,7 @@ class NotificationRecipient
return false unless @target
return false unless @target.respond_to?(:subscriptions)
- subscription = @target.subscriptions.find_by_user_id(@user.id)
+ subscription = @target.subscriptions.find { |subscription| subscription.user_id == @user.id }
subscription && !subscription.subscribed
end
@@ -142,10 +144,33 @@ class NotificationRecipient
return project_setting unless project_setting.nil? || project_setting.global?
- group_setting = @group && user.notification_settings_for(@group)
+ group_setting = closest_non_global_group_notification_settting
- return group_setting unless group_setting.nil? || group_setting.global?
+ return group_setting unless group_setting.nil?
user.global_notification_setting
end
+
+ # Returns the notificaton_setting of the lowest group in hierarchy with non global level
+ def closest_non_global_group_notification_settting
+ return unless @group
+ return if indexed_group_notification_settings.empty?
+
+ notification_setting = nil
+
+ @group.self_and_ancestors_ids.each do |id|
+ notification_setting = indexed_group_notification_settings[id]
+ break if notification_setting
+ end
+
+ notification_setting
+ end
+
+ def indexed_group_notification_settings
+ strong_memoize(:indexed_group_notification_settings) do
+ @group.notification_settings.where(user_id: user.id)
+ .where.not(level: NotificationSetting.levels[:global])
+ .index_by(&:source_id)
+ end
+ end
end
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index ed6c1eddbc1..d7d6aaceb27 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -1,11 +1,18 @@
class ProjectAutoDevops < ActiveRecord::Base
belongs_to :project
+ enum deploy_strategy: {
+ continuous: 0,
+ manual: 1
+ }
+
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
+ after_save :create_gitlab_deploy_token, if: :needs_to_create_deploy_token?
+
def instance_domain
Gitlab::CurrentSettings.auto_devops_domain
end
@@ -20,6 +27,30 @@ class ProjectAutoDevops < ActiveRecord::Base
variables.append(key: 'AUTO_DEVOPS_DOMAIN',
value: domain.presence || instance_domain)
end
+
+ if manual?
+ variables.append(key: 'STAGING_ENABLED', value: 1)
+ variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 1)
+ end
end
end
+
+ private
+
+ def create_gitlab_deploy_token
+ project.deploy_tokens.create!(
+ name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME,
+ read_registry: true
+ )
+ end
+
+ def needs_to_create_deploy_token?
+ auto_devops_enabled? &&
+ !project.public? &&
+ !project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present?
+ end
+
+ def auto_devops_enabled?
+ Gitlab::CurrentSettings.auto_devops_enabled? || enabled?
+ end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index eb3261c902f..412d62388f0 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -265,7 +265,7 @@ class JiraService < IssueTrackerService
title: title,
status: status,
icon: {
- title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url)
+ title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.url)
}
}
}
diff --git a/app/models/user.rb b/app/models/user.rb
index e219ab800ad..8e0dc91b2a7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1038,7 +1038,10 @@ class User < ActiveRecord::Base
def notification_settings_for(source)
if notification_settings.loaded?
- notification_settings.find { |notification| notification.source == source }
+ notification_settings.find do |notification|
+ notification.source_type == source.class.base_class.name &&
+ notification.source_id == source.id
+ end
else
notification_settings.find_or_initialize_by(source: source)
end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index 8e8bda2f9df..47df7f9dcf9 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -7,16 +7,7 @@ class StatusEntity < Grape::Entity
expose :details_path
expose :favicon do |status|
- dir =
- if Gitlab::Utils.to_boolean(ENV['CANARY'])
- File.join('ci_favicons', 'canary')
- elsif Rails.env.development?
- File.join('ci_favicons', 'dev')
- else
- 'ci_favicons'
- end
-
- ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
+ Gitlab::Favicon.status_overlay(status.favicon)
end
expose :action, if: -> (status, _) { status.has_action? } do
diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb
index e67af929954..94a434b95dd 100644
--- a/app/services/applications/create_service.rb
+++ b/app/services/applications/create_service.rb
@@ -5,7 +5,7 @@ module Applications
@params = params.except(:ip_address)
end
- def execute(request = nil)
+ def execute(request)
Doorkeeper::Application.create(@params)
end
end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 5a961ac89e4..b1dbe73cdf7 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -3,13 +3,18 @@ module Boards
class ListService < Boards::BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
- issues = without_board_labels(issues) unless movable_list? || closed_list?
- issues = with_list_label(issues) if movable_list?
+ issues = filter(issues)
issues.order_by_position_and_priority
end
private
+ def filter(issues)
+ issues = without_board_labels(issues) unless list&.movable? || list&.closed?
+ issues = with_list_label(issues) if list&.label?
+ issues
+ end
+
def board
@board ||= parent.boards.find(params[:board_id])
end
@@ -20,18 +25,6 @@ module Boards
@list = board.lists.find(params[:id]) if params.key?(:id)
end
- def movable_list?
- return @movable_list if defined?(@movable_list)
-
- @movable_list = list.present? && list.movable?
- end
-
- def closed_list?
- return @closed_list if defined?(@closed_list)
-
- @closed_list = list.present? && list.closed?
- end
-
def filter_params
set_parent
set_state
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 3ceab209f3f..ee3112c7571 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -3,7 +3,7 @@ module Boards
class MoveService < Boards::BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
- return false if issue_params.empty?
+ return false if issue_params(issue).empty?
update(issue)
end
@@ -28,10 +28,10 @@ module Boards
end
def update(issue)
- ::Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
+ ::Issues::UpdateService.new(issue.project, current_user, issue_params(issue)).execute(issue)
end
- def issue_params
+ def issue_params(issue)
attrs = {}
if move_between_lists?
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index 02f1c709374..6fd9885d4f3 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -1,16 +1,28 @@
module Boards
module Lists
class CreateService < Boards::BaseService
+ include Gitlab::Utils::StrongMemoize
+
def execute(board)
List.transaction do
- label = available_labels_for(board).find(params[:label_id])
+ target = target(board)
position = next_position(board)
- create_list(board, label, position)
+ create_list(board, type, target, position)
end
end
private
+ def type
+ :label
+ end
+
+ def target(board)
+ strong_memoize(:target) do
+ available_labels_for(board).find(params[:label_id])
+ end
+ end
+
def available_labels_for(board)
options = { include_ancestor_groups: true }
@@ -28,8 +40,8 @@ module Boards
max_position.nil? ? 0 : max_position.succ
end
- def create_list(board, label, position)
- board.lists.create(label: label, list_type: :label, position: position)
+ def create_list(board, type, target, position)
+ board.lists.create(type => target, list_type: type, position: position)
end
end
end
diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb
new file mode 100644
index 00000000000..09afc63a5aa
--- /dev/null
+++ b/app/uploaders/favicon_uploader.rb
@@ -0,0 +1,24 @@
+class FaviconUploader < AttachmentUploader
+ EXTENSION_WHITELIST = %w[png ico].freeze
+
+ include CarrierWave::MiniMagick
+
+ version :favicon_main do
+ process resize_to_fill: [32, 32]
+ process convert: 'png'
+
+ def full_filename(filename)
+ filename_for_different_format(super(filename), 'png')
+ end
+ end
+
+ def extension_whitelist
+ EXTENSION_WHITELIST
+ end
+
+ private
+
+ def filename_for_different_format(filename, format)
+ filename.chomp(File.extname(filename)) + ".#{format}"
+ end
+end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index fd446d31092..207928b61d0 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,6 +1,6 @@
# Extra methods for uploader
module UploaderHelper
- IMAGE_EXT = %w[png jpg jpeg gif bmp tiff].freeze
+ IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze
# We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 5e08837255f..94db374040c 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -11,13 +11,32 @@
= image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
%hr
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: ""
.hint
Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
+ %fieldset.app_logo
+ %legend
+ Favicon:
+ .form-group.row
+ = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label'
+ .col-sm-10
+ - if @appearance.favicon?
+ = image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
+ %hr
+ = f.hidden_field :favicon_cache
+ = f.file_field :favicon, class: ''
+ .hint
+ Maximum file size is 1MB. Allowed image formats are #{favicon_extension_whitelist}.
+ %br
+ The resulting favicons will be cropped to be square and scaled down to a size of 32x32 px.
+
%fieldset.sign-in
%legend
Sign in/Sign up pages:
@@ -38,7 +57,7 @@
= image_tag @appearance.logo_url, class: 'appearance-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
%hr
= f.hidden_field :logo_cache
= f.file_field :logo, class: ""
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index dc4dccc9e0d..c8008771236 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -2,6 +2,9 @@
= form_errors(@group)
= render 'shared/group_form', f: f
+ = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
+ = render_if_exists 'admin/namespace_plan', f: f
+
.form-group.row.group-description-holder
= f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2'
.col-sm-10
@@ -15,6 +18,8 @@
= render 'groups/group_admin_settings', f: f
+ = render_if_exists 'namespaces/shared_runners_minutes_settings', group: @group, form: f
+
- if @group.new_record?
.form-group.row
.offset-sm-2.col-sm-10
@@ -28,3 +33,5 @@
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
= link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel"
+
+= render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index e7c70a6f187..3f96988c203 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -1,3 +1,4 @@
+- group = local_assigns.fetch(:group)
- css_class = 'no-description' if group.description.blank?
%li.group-row{ class: css_class }
@@ -8,6 +9,8 @@
%span.badge.badge-pill
= storage_counter(group.storage_size)
+ = render_if_exists 'admin/namespace_plan_badge', namespace: group
+
%span
= icon('bookmark')
= number_with_delimiter(group.projects.count)
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 6d75ccd5add..a40f98ad24f 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -40,6 +40,8 @@
%strong
= @group.created_at.to_s(:medium)
+ = render_if_exists 'admin/namespace_plan_info', namespace: @group
+
%li
%span.light Storage:
%strong= storage_counter(@group.storage_size)
@@ -58,6 +60,10 @@
= group_lfs_status(@group)
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ = render_if_exists 'namespaces/shared_runner_status', namespace: @group
+
+ = render_if_exists 'ldap_group_links/ldap_group_links_show', group: @group
+
.card
.card-header
%h3.card-title
@@ -104,7 +110,7 @@
= form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
- = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
+ = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all)
.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index 144dceacbdd..993006e8745 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -7,5 +7,4 @@
= render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block
- .form-actions
- = form.submit 'Save', class: 'btn btn-save'
+ = form.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index ac7e12fcd0b..db7eaff6658 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,21 +1,32 @@
-- page_title 'Labels'
-
+- @no_container = true
+- page_title "Labels"
+- can_admin_label = can?(current_user, :admin_label, @group)
+- hide_class = ''
+- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
- issuables = ['issues', 'merge requests']
-.top-area.adjust
- .nav-text
- = _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence }
+- if can_admin_label
+ - content_for(:header_content) do
+ .nav-controls
+ = link_to _('New label'), new_group_label_path(@group), class: "btn btn-new"
+
+- if @labels.exists?
+ #promote-label-modal
+ %div{ class: container_class }
+ .top-area.adjust
+ .nav-text
+ = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence }
- .nav-controls
- - if can?(current_user, :admin_label, @group)
- = link_to "New label", new_group_label_path(@group), class: "btn btn-new"
+ .labels-container.prepend-top-5
+ .other-labels
+ - if can_admin_label
+ %h5{ class: ('hide' if hide) } Labels
+ %ul.content-list.manage-labels-list.js-other-labels
+ = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false }
+ = paginate @labels, theme: 'gitlab'
+- else
+ = render 'shared/empty_states/labels'
-.labels
- .other-labels
- - if @labels.present?
- %ul.content-list.manage-labels-list.js-other-labels
- = render partial: 'shared/label', subject: @group, collection: @labels, as: :label
- = paginate @labels, theme: 'gitlab'
- - else
- .nothing-here-block
- = _("No labels created yet.")
+%template#js-badge-item-template
+ %li.label-link-item.js-priority-badge.inline.prepend-left-10
+ .label-badge.label-badge-blue= _('Prioritized label')
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 02bdfe9aa3c..9253a0652da 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -25,7 +25,7 @@
%title= page_title(site_name)
%meta{ name: "description", content: page_description }
- = favicon_link_tag favicon, id: 'favicon'
+ = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png'
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index ca7a6d5a886..59c4eeec17a 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -15,7 +15,7 @@
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group
- = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
+ = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project')
.js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
= provider_gcp_field.hidden_field :gcp_project_id
.dropdown
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index 2e92524ce8f..db57da99ec7 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -1,27 +1,27 @@
= form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
- = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light'
= field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light'
= platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
.form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light'
= platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
.form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light'
= platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
.form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
index 77d7a055474..4d117f435dc 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -1,20 +1,20 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
.form-group
- = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
+ = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
- = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
+ = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light'
= platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
.form-group
- = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
+ = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light'
= platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
.form-group
- = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token')
+ = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light'
.input-group
= platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off'
%span.input-group-append.clipboard-addon
@@ -23,7 +23,7 @@
= s_('ClusterIntegration|Show')
.form-group
- = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
+ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
.form-group
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 9c78bade254..fb5b0fc15c9 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,40 +1,44 @@
- @no_container = true
- page_title "Labels"
-- hide_class = ''
- can_admin_label = can?(current_user, :admin_label, @project)
+- hide_class = ''
+
+- if can_admin_label
+ - content_for(:header_content) do
+ .nav-controls
+ = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new"
- if @labels.exists? || @prioritized_labels.exists?
#promote-label-modal
%div{ class: container_class }
.top-area.adjust
.nav-text
- Labels can be applied to issues and merge requests.
+ = _('Labels can be applied to issues and merge requests.')
- if can_admin_label
- Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
+ = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.')
- - if can_admin_label
- .nav-controls
- = link_to new_project_label_path(@project), class: "btn btn-new" do
- New label
-
- .labels
+ .labels-container.prepend-top-5
- if can_admin_label
-# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
.prioritized-labels{ class: ('hide' if hide) }
- %h5 Prioritized Labels
- %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) }
- #js-priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" }
+ %h5.prepend-top-10= _('Prioritized Labels')
+ .content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) }
+ #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" }
= render 'shared/empty_states/priority_labels'
- if @prioritized_labels.present?
- = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label
+ = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true }
- if @labels.present?
.other-labels
- if can_admin_label
- %h5{ class: ('hide' if hide) } Other Labels
- %ul.content-list.manage-labels-list.js-other-labels
+ %h5{ class: ('hide' if hide) }= _('Other Labels')
+ .content-list.manage-labels-list.js-other-labels
= render partial: 'shared/label', subject: @project, collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- else
= render 'shared/empty_states/labels'
+
+%template#js-badge-item-template
+ %li.label-link-item.js-priority-badge.inline.prepend-left-10
+ .label-badge.label-badge-blue= _('Prioritized label')
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 118391aac64..951f80b378d 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,7 +1,7 @@
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
%li.js-pipeline-tab-link
- = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
+ = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
= _("Pipeline")
%li.js-builds-tab-link
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
@@ -43,12 +43,36 @@
= render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage
- if @pipeline.failed_builds.present?
- #js-tab-failures.build-failures.tab-pane
- - @pipeline.failed_builds.each_with_index do |build, index|
- .build-state
- %span.ci-status-icon-failed= custom_icon('icon_status_failed')
- %span.stage
- = build.stage.titleize
- %span.build-name
- = link_to build.name, pipeline_job_url(pipeline, build)
- %pre.build-log= build_summary(build, skip: index >= 10)
+ #js-tab-failures.build-failures.tab-pane.build-page
+ %table.table.responsive-table.ci-table.responsive-table-sm-rounded
+ %thead
+ %th.table-th-transparent
+ %th.table-th-transparent= _("Name")
+ %th.table-th-transparent= _("Stage")
+ %th.table-th-transparent= _("Failure")
+
+ %tbody
+ - @pipeline.failed_builds.each_with_index do |build, index|
+ - job = build.present(current_user: current_user)
+ %tr.build-state.responsive-table-border-start
+ %td.responsive-table-cell.ci-status-icon-failed{ data: { column: "Status"} }
+ .d-none.d-md-block.build-icon
+ = custom_icon("icon_status_#{build.status}")
+ .d-md-none.build-badge
+ = render "ci/status/badge", link: false, status: job.detailed_status(current_user)
+ %td.responsive-table-cell.build-name{ data: { column: _("Name")} }
+ = link_to build.name, pipeline_job_url(pipeline, build)
+ %td.responsive-table-cell.build-stage{ data: { column: _("Stage")} }
+ = build.stage.titleize
+ %td.responsive-table-cell.build-failure{ data: { column: _("Failure")} }
+ = build.present.callout_failure_message
+ %td.responsive-table-cell.build-actions
+ = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do
+ = icon('repeat')
+ %tr.build-trace-row.responsive-table-border-end
+ %td
+ %td.responsive-table-cell.build-trace-container{ colspan: 4 }
+ %pre.build-trace.build-trace-rounded
+ %code.bash.js-build-output
+ = build_summary(build)
+
diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml
index 128f52ff648..3f05e06b0c6 100644
--- a/app/views/projects/project_members/_groups.html.haml
+++ b/app/views/projects/project_members/_groups.html.haml
@@ -3,5 +3,5 @@
Groups with access to
%strong= @project.name
%span.badge.badge-pill= group_links.size
- %ul.content-list
+ %ul.content-list.members-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index a2cd7752fc4..9a06eca89bb 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -1,7 +1,7 @@
.protected-branches-list.js-protected-branches-list.qa-protected-branches-list
- if @protected_branches.empty?
- .card-header
- %h3.card-title
+ .card-header.bg-white
+ %h3.card-title.mb-0
Protected branch (#{@protected_branches_count})
%p.settings-message.text-center
There are currently no protected branches, protect a branch with the form above.
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index 62bef77be97..b20614dc88f 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,18 +1,19 @@
- enabled = Gitlab.config.mattermost.enabled
-.card
- %p
- This service allows users to perform common operations on this
- project by entering slash commands in Mattermost.
- = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
- View documentation
- = icon('external-link')
- %p.inline
- See list of available commands in Mattermost after setting up this service,
- by entering
- %kbd.inline /&lt;trigger&gt; help
- - unless enabled || @service.template?
- = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
+.info-well
+ .well-segment
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Mattermost.
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Mattermost after setting up this service,
+ by entering
+ %kbd.inline /&lt;trigger&gt; help
+ - unless enabled || @service.template?
+ = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
- if enabled && !@service.template?
= render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
diff --git a/app/views/projects/services/prometheus/_configuration_banner.html.haml b/app/views/projects/services/prometheus/_configuration_banner.html.haml
index 2cc2a6b2b5b..898b55e4b39 100644
--- a/app/views/projects/services/prometheus/_configuration_banner.html.haml
+++ b/app/views/projects/services/prometheus/_configuration_banner.html.haml
@@ -2,7 +2,7 @@
= s_('PrometheusService|Auto configuration')
- if service.manual_configuration?
- .well
+ .info-well
= s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
- else
.container-fluid
diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/projects/services/prometheus/_help.html.haml
index 15e7362c2ba..35d655e4b32 100644
--- a/app/views/projects/services/prometheus/_help.html.haml
+++ b/app/views/projects/services/prometheus/_help.html.haml
@@ -5,5 +5,5 @@
= s_('PrometheusService|Manual configuration')
- unless @service.editable?
- .card
+ .info-well
= s_('PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters')
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 44f58ad05e5..9d045d84b52 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,99 +1,100 @@
- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
-.card
- %p
- This service allows users to perform common operations on this
- project by entering slash commands in Slack.
- = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
- View documentation
- = icon('external-link')
- %p.inline
- See list of available commands in Slack after setting up this service,
- by entering
- %kbd.inline /&lt;command&gt; help
- - unless @service.template?
- %p To setup this service:
- %ul.list-unstyled.indent-list
- %li
- 1.
- = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
- Add a slash command
- = icon('external-link')
- in your Slack team with these options:
+.info-well
+ .well-segment
+ %p
+ This service allows users to perform common operations on this
+ project by entering slash commands in Slack.
+ = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
+ View documentation
+ = icon('external-link')
+ %p.inline
+ See list of available commands in Slack after setting up this service,
+ by entering
+ %kbd.inline /&lt;command&gt; help
+ - unless @service.template?
+ %p To setup this service:
+ %ul.list-unstyled.indent-list
+ %li
+ 1.
+ = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ Add a slash command
+ = icon('external-link')
+ in your Slack team with these options:
- %hr
+ %hr
- .help-form
- .form-group
- = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block
- %p Fill in the word that works best for your team.
- %p
- Suggestions:
- %code= 'gitlab'
- %code= @project.path # Path contains no spaces, but dashes
- %code= @project.full_path
+ .help-form
+ .form-group
+ = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block
+ %p Fill in the word that works best for your team.
+ %p
+ Suggestions:
+ %code= 'gitlab'
+ %code= @project.path # Path contains no spaces, but dashes
+ %code= @project.full_path
- .form-group
- = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
- = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
- .input-group-append
- = clipboard_button(target: '#url', class: 'input-group-text')
+ .form-group
+ = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#url', class: 'input-group-text')
- .form-group
- = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block POST
+ .form-group
+ = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block POST
- .form-group
- = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
- = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
- .input-group-append
- = clipboard_button(target: '#customize_name', class: 'input-group-text')
+ .form-group
+ = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#customize_name', class: 'input-group-text')
- .form-group
- = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block
- = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
- = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
+ .form-group
+ = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block
+ = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
+ = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
- .form-group
- = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.text-block Show this command in the autocomplete list
+ .form-group
+ = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.text-block Show this command in the autocomplete list
- .form-group
- = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
- = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
- .input-group-append
- = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
+ .form-group
+ = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_description', class: 'input-group-text')
- .form-group
- = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
- = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
- .input-group-append
- = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
+ .form-group
+ = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text')
- .form-group
- = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
- .col-sm-10.col-12.input-group
- = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
- .input-group-append
- = clipboard_button(target: '#descriptive_label', class: 'input-group-text')
+ .form-group
+ = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label'
+ .col-sm-10.col-12.input-group
+ = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly'
+ .input-group-append
+ = clipboard_button(target: '#descriptive_label', class: 'input-group-text')
- %hr
+ %hr
- %ul.list-unstyled.indent-list
- %li
- 2. Paste the
- %strong Token
- into the field below
- %li
- 3. Select the
- %strong Active
- checkbox, press
- %strong Save changes
- and start using GitLab inside Slack!
+ %ul.list-unstyled.indent-list
+ %li
+ 2. Paste the
+ %strong Token
+ into the field below
+ %li
+ 3. Select the
+ %strong Active
+ checkbox, press
+ %strong Save changes
+ and start using GitLab inside Slack!
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index bbabb98dafe..4359362bb05 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -1,43 +1,66 @@
-.row.prepend-top-default
+.row
.col-lg-12
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
- %fieldset.builds-feature
+ %fieldset.builds-feature.js-auto-devops-settings
.form-group
- message = auto_devops_warning_message(@project)
- ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
- if message
- %p.settings-message.text-center
+ %p.auto-devops-warning-message.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
- .form-check
- = form.radio_button :enabled, 'true', class: 'form-check-input'
- = form.label :enabled_true, class: 'form-check-label' do
- %strong= s_('CICD|Enable Auto DevOps')
- %br
- = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
+ .card.auto-devops-card
+ .card-body
+ .form-check
+ = form.radio_button :enabled, 'true', class: 'form-check-input js-toggle-extra-settings'
+ = form.label :enabled_true, class: 'form-check-label' do
+ %strong= s_('CICD|Enable Auto DevOps')
+ .form-text.text-muted
+ = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
- .form-check
- = form.radio_button :enabled, 'false', class: 'form-check-input'
- = form.label :enabled_false, class: 'form-check-label' do
- %strong= s_('CICD|Disable Auto DevOps')
- %br
- = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
+ .card.auto-devops-card
+ .card-body
+ .form-check
+ = form.radio_button :enabled, '', class: 'form-check-input js-toggle-extra-settings'
+ = form.label :enabled_, class: 'form-check-label' do
+ %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
+ .form-text.text-muted
+ = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
- .form-check
- = form.radio_button :enabled, '', class: 'form-check-input'
- = form.label :enabled_, class: 'form-check-label' do
- %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
- %br
- = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
+ .card.auto-devops-card.js-extra-settings{ class: form.object&.enabled == false ? 'hidden' : nil }
+ .card-body.bg-light
+ = form.label :domain do
+ %strong= _('Domain')
+ = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
+ .form-text.text-muted
+ = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
+ - if cluster_ingress_ip = cluster_ingress_ip(@project)
+ = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
- = form.label :domain, class:"prepend-top-10" do
- = _('Domain')
- = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
- .form-text.text-muted
- = s_('CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages.')
- - if cluster_ingress_ip = cluster_ingress_ip(@project)
- = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
- = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank'
+ %label.prepend-top-10
+ %strong= s_('CICD|Deployment strategy')
+ %p.settings-message.text-center
+ = s_('CICD|Deployment strategy needs a domain name to work correctly.')
+ .form-check
+ = form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input'
+ = form.label :deploy_strategy_continuous, class: 'form-check-label' do
+ %strong= s_('CICD|Continuous deployment to production')
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-deploy'), target: '_blank'
+ .form-check
+ = form.radio_button :deploy_strategy, 'manual', class: 'form-check-input'
+ = form.label :deploy_strategy_manual, class: 'form-check-label' do
+ %strong= s_('CICD|Automatic deployment to staging, manual deployment to production')
+ = link_to icon('question-circle'), help_page_path('ci/environments.md', anchor: 'manually-deploying-to-environments'), target: '_blank'
+
+ .card.auto-devops-card
+ .card-body
+ .form-check
+ = form.radio_button :enabled, 'false', class: 'form-check-input js-toggle-extra-settings', data: { hide_extra_settings: true }
+ = form.label :enabled_false, class: 'form-check-label' do
+ %strong= s_('CICD|Disable Auto DevOps')
+ .form-text.text-muted
+ = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
= f.submit 'Save changes', class: "btn btn-success prepend-top-15"
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index 0c8d90d92f5..b89045e726a 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -9,11 +9,11 @@
- help = field[:help]
- disabled = disable_fields_service?(@service)
-.form-group
+.form-group.row
- if type == "password" && value.present?
- = form.label name, "Enter new #{title.downcase}", class: "col-form-label"
+ = form.label name, "Enter new #{title.downcase}", class: "col-form-label col-sm-2"
- else
- = form.label name, title, class: "col-form-label"
+ = form.label name, title, class: "col-form-label col-sm-2"
.col-sm-10
- if type == 'text'
= form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index ba5b65a209d..5eec7b02b54 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,93 +1,70 @@
- label_css_id = dom_id(label)
- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
+- use_label_priority = local_assigns.fetch(:use_label_priority, false)
+- force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority.present? : false)
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
+- tooltip_title = label_status_tooltip(label, status) if status
%li.label-list-item{ id: label_css_id, data: { id: label.id } }
- = render "shared/label_row", label: label
-
- .d-inline-block.d-sm-none.dropdown
- %button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-right
- %ul
- - if show_label_merge_requests_link
- %li
- = link_to_label(label, subject: subject, type: :merge_request) do
- View merge requests
- - if show_label_issues_link
- %li
- = link_to_label(label, subject: subject) do
- View open issues
- - if current_user
- %li.label-subscription
- - if can_subscribe_to_label_in_different_levels?(label)
- %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } }
- %span Unsubscribe
- %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } }
- %span Subscribe at project level
- %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } }
- %span Subscribe at group level
- - else
- %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_path } }
- %span= label_subscription_toggle_button_text(label, @project)
-
- - if can?(current_user, :admin_label, label)
- %li
- = link_to 'Edit', edit_label_path(label)
- %li
- = link_to 'Delete',
- destroy_label_path(label),
- title: 'Delete',
- method: :delete,
- data: {confirm: 'Remove this label? Are you sure?'},
- class: 'text-danger'
-
- .float-right.d-none.d-sm-none.d-md-block
- - if can?(current_user, :admin_label, label)
- - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
- %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'),
- disabled: true,
- type: 'button',
- data: { url: promote_project_label_path(label.project, label),
- label_title: label.title,
- label_color: label.color,
- label_text_color: label.text_color,
- group_name: label.project.group.name,
- target: '#promote-label-modal',
- container: 'body',
- toggle: 'modal' } }
- = sprite_icon('level-up')
- = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
- %span.sr-only Edit
- = sprite_icon('pencil')
- %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
- = link_to "#", title: "Delete", class: 'btn btn-transparent btn-action remove-row', data: { toggle: "tooltip" } do
- %span.sr-only Delete
- = sprite_icon('remove')
+ = render "shared/label_row", label: label, subject: subject, force_priority: force_priority
+ %ul.label-actions-list
+ - if @project
+ %li.inline
+ .label-badge.label-badge-gray= label.model_name.human.capitalize
+ - if can?(current_user, :admin_label, @project)
+ %li.inline.js-toggle-priority{ data: { url: remove_priority_project_label_path(@project, label),
+ dom_id: dom_id(label), type: label.type } }
+ %button.label-action.add-priority.btn.btn-transparent.has-tooltip{ title: _('Prioritize'), type: 'button', data: { placement: 'top' }, aria_label: _('Prioritize label') }
+ = sprite_icon('star-o')
+ %button.label-action.remove-priority.btn.btn-transparent.has-tooltip{ title: _('Remove priority'), type: 'button', data: { placement: 'top' }, aria_label: _('Deprioritize label') }
+ = sprite_icon('star')
+ %li.inline
+ = link_to edit_label_path(label), class: 'btn btn-transparent label-action', aria_label: 'Edit label' do
+ = sprite_icon('pencil')
+ %li.inline
+ .dropdown
+ %button{ type: 'button', class: 'btn btn-transparent js-label-options-dropdown label-action', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') }
+ = sprite_icon('ellipsis_v')
+ .dropdown-menu.dropdown-open-left
+ %ul
+ - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
+ %li
+ %button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button',
+ data: { url: promote_project_label_path(label.project, label),
+ label_title: label.title,
+ label_color: label.color,
+ label_text_color: label.text_color,
+ group_name: label.project.group.name,
+ target: '#promote-label-modal',
+ container: 'body',
+ toggle: 'modal' } }
+ = _('Promote to group label')
+ %li
+ %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
+ %button.text-danger.remove-row{ type: 'button' }= _('Delete')
- if current_user
- .label-subscription.inline
+ %li.inline.label-subscription
- if can_subscribe_to_label_in_different_levels?(label)
- %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } }
- %span Unsubscribe
- = icon('spinner spin', class: 'label-subscribe-button-loading')
-
+ %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
+ %span= _('Unsubscribe')
.dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
- %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span Subscribe
- = icon('chevron-down')
- %ul.dropdown-menu
- %li
- %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } }
- Project level
- %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } }
- Group level
+ %button.label-subscribe-button.btn.btn-default{ data: { toggle: 'dropdown' } }
+ %span
+ = _('Subscribe')
+ = sprite_icon('chevron-down')
+ .dropdown-menu.dropdown-open-left
+ %ul
+ %li
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_project_label_path(@project, label) } }
+ %span= _('Subscribe at project level')
+ %li
+ %button.js-subscribe-button.js-group-level.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } }
+ %span= _('Subscribe at group level')
- else
- %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_path } }
+ %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title }
%span= label_subscription_toggle_button_text(label, @project)
- = icon('spinner spin', class: 'label-subscribe-button-loading')
= render 'shared/delete_label_modal', label: label
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index f1c1ca9b2c9..0ae3ab8f090 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -1,30 +1,23 @@
- subject = local_assigns[:subject]
+- force_priority = local_assigns.fetch(:force_priority, false)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
-%span.label-row
- - if can?(current_user, :admin_label, @project)
- .draggable-handler
- = icon('bars')
- .js-toggle-priority.toggle-priority{ data: { url: remove_priority_project_label_path(@project, label),
- dom_id: dom_id(label), type: label.type } }
- %button.add-priority.btn.has-tooltip{ title: 'Prioritize', type: 'button', :'data-placement' => 'top' }
- = icon('star-o')
- %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', type: 'button', :'data-placement' => 'top' }
- = icon('star')
- %span.label-name
- = link_to_label(label, subject: @project, tooltip: false)
- - if defined?(@project) && @project.group.present?
- %span.label-type
- = label.model_name.human.titleize
-
- %span.label-description
+.label-name
+ = link_to_label(label, subject: @project, tooltip: false)
+.label-description
+ .append-right-default.prepend-left-default
- if label.description.present?
- .description-text
+ .description-text.append-bottom-10
= markdown_field(label, :description)
- .d-none.d-sm-none.d-md-block
+ %ul.label-links
- if show_label_issues_link
- = link_to_label(label, subject: subject) { 'Issues' }
+ %li.label-link-item.inline
+ = link_to_label(label, subject: subject) { 'Issues' }
- if show_label_merge_requests_link
&middot;
- = link_to_label(label, subject: subject, type: :merge_request) { 'Merge requests' }
+ %li.label-link-item.inline
+ = link_to_label(label, subject: subject, type: :merge_request) { _('Merge requests') }
+ - if force_priority
+ %li.label-link-item.js-priority-badge.inline.prepend-left-10
+ .label-badge.label-badge-blue= _('Prioritized label')
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 0ebf365c7bd..6fa61c15493 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -3,8 +3,9 @@
- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
= render "projects/services/#{@service.to_param}/help", subject: subject
- elsif @service.help.present?
- .card
- = markdown @service.help
+ .info-well
+ .well-segment
+ = markdown @service.help
.service-settings
- if @service.show_active_box?
@@ -15,25 +16,24 @@
- if @service.configurable_events.present?
.form-group.row
- = form.label :url, "Trigger", class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right Trigger
.col-sm-10
- @service.configurable_events.each do |event|
- %div
- = form.check_box service_event_field_name(event), class: 'float-left'
- .prepend-left-20
- = form.label service_event_field_name(event), class: 'list-label' do
+ .form-group
+ .form-check
+ = form.check_box service_event_field_name(event), class: 'form-check-input'
+ = form.label service_event_field_name(event), class: 'form-check-label' do
%strong
= event.humanize
- - field = @service.event_field(event)
+ - field = @service.event_field(event)
- - if field
- %p
+ - if field
= form.text_field field[:name], class: "form-control", placeholder: field[:placeholder]
- %p.light
- = @service.class.event_description(event)
+ %p.text-muted
+ = @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 67476a3f573..76843ce7cc0 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -1,4 +1,4 @@
-.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded }',
+.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }',
":data-id" => "list.id" }
.board-inner
%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)" }
@@ -7,10 +7,18 @@
":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded }",
"aria-hidden": "true" }
+ %a.user-avatar-link.js-no-trigger{ "v-if": "list.type === \"assignee\"", ":href": "list.assignee.path" }
+ -# haml-lint:disable AltText
+ %img.avatar.s20.has-tooltip{ height: "20", width: "20", ":src": "list.assignee.avatar", ":alt": "list.assignee.name" }
+
%span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"",
- ":title" => '(list.label ? list.label.description : "")', data: { container: "body" } }
+ ":title" => '((list.label && list.label.description) || list.title || "")', data: { container: "body" } }
{{ list.title }}
+ %span.board-title-sub-text.prepend-left-5.has-tooltip{ "v-if": "list.type === \"assignee\"",
+ ":title" => '(list.assignee && list.assignee.username || "")' }
+ @{{ list.assignee.username }}
+
%span.has-tooltip{ "v-if": "list.type === \"label\"",
":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" },
diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
new file mode 100644
index 00000000000..23b2e1b91e5
--- /dev/null
+++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml
@@ -0,0 +1,8 @@
+.dropdown.prepend-left-10#js-add-list
+ %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
+ Add list
+ .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
+ = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
+ - if can?(current_user, :admin_label, board.parent)
+ = render partial: "shared/issuable/label_page_create"
+ = dropdown_loading
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index 9f4021802df..55edaa7eda4 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -1,6 +1,7 @@
+- show_close = local_assigns.fetch(:show_close, true)
- subject = @project || @group
.dropdown-page-two.dropdown-new-label
- = dropdown_title(create_label_title(subject), options: { back: true })
+ = dropdown_title(create_label_title(subject), options: { back: true, close: show_close })
= dropdown_content do
.dropdown-labels-error.js-label-error
%input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') }
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index 2bd922bca2b..aa4a5f0e0d3 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -1,15 +1,18 @@
- title = local_assigns.fetch(:title, _('Assign labels'))
+- content_title = local_assigns.fetch(:content_title, _('Create lists from labels. Issues with that label appear in that list.'))
+- show_title = local_assigns.fetch(:show_title, true)
- show_create = local_assigns.fetch(:show_create, true)
- show_footer = local_assigns.fetch(:show_footer, true)
- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search')
- show_boards_content = local_assigns.fetch(:show_boards_content, false)
- subject = @project || @group
.dropdown-page-one
- = dropdown_title(title)
+ - if show_title
+ = dropdown_title(title)
- if show_boards_content
.issue-board-dropdown-content
%p
- = _('Create lists from labels. Issues with that label appear in that list.')
+ = content_title
= dropdown_filter(filter_placeholder)
= dropdown_content
- if current_board_parent && show_footer
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 644f7c4dd28..ef9ea2194ee 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -104,14 +104,7 @@
.filter-dropdown-container
- if type == :boards
- if can?(current_user, :admin_list, board.parent)
- .dropdown.prepend-left-10#js-add-list
- %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
- Add list
- .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- - if can?(current_user, :admin_label, board.parent)
- = render partial: "shared/issuable/label_page_create"
- = dropdown_loading
+ = render_if_exists 'shared/issuable/board_create_list_dropdown', board: board
- if @project
#js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
- elsif type != :boards_modal
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index 67b8843a27f..d0b492b43f3 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -5,16 +5,16 @@
%li.member.group_member{ id: dom_id }
%span.list-item-name
= group_icon(group, class: "avatar s40", alt: '')
- %strong
- = link_to group.full_name, group_path(group)
- .cgray
- Given access #{time_ago_with_tooltip(group_link.created_at)}
- - if group_link.expires?
- ·
- %span{ class: ('text-warning' if group_link.expires_soon?) }
- Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
+ .user-info
+ = link_to group.full_name, group_path(group), class: 'member'
+ .cgray
+ Given access #{time_ago_with_tooltip(group_link.created_at)}
+ - if group_link.expires?
+ ·
+ %span{ class: ('text-warning' if group_link.expires_soon?) }
+ Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.controls.member-controls
- = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form' do
+ = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group row append-right-5' do
= hidden_field_tag "group_link[group_access]", group_link.group_access
.member-form-control.dropdown.append-right-5
%button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button",
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index ae437dd16d6..2d0bb722189 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -5,6 +5,6 @@
- scopes.each do |scope|
%fieldset
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
- = label_tag ("#{prefix}_scopes_#{scope}"), scope
+ = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: "label-light"
%span= t(scope, scope: [:doorkeeper, :scopes])
.scope-description= t scope, scope: [:doorkeeper, :scope_desc]
diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb
index f92421a667d..0aff0c4c7c6 100644
--- a/app/workers/storage_migrator_worker.rb
+++ b/app/workers/storage_migrator_worker.rb
@@ -1,29 +1,8 @@
class StorageMigratorWorker
include ApplicationWorker
- BATCH_SIZE = 100
-
def perform(start, finish)
- projects = build_relation(start, finish)
-
- projects.with_route.find_each(batch_size: BATCH_SIZE) do |project|
- Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..."
-
- begin
- project.migrate_to_hashed_storage!
- rescue => err
- Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
- end
- end
- end
-
- def build_relation(start, finish)
- relation = Project
- table = Project.arel_table
-
- relation = relation.where(table[:id].gteq(start)) if start
- relation = relation.where(table[:id].lteq(finish)) if finish
-
- relation
+ migrator = Gitlab::HashedStorage::Migrator.new
+ migrator.bulk_migrate(start, finish)
end
end
diff --git a/changelogs/unreleased/38542-application-control-panel-in-settings-page.yml b/changelogs/unreleased/38542-application-control-panel-in-settings-page.yml
new file mode 100644
index 00000000000..0654456ea45
--- /dev/null
+++ b/changelogs/unreleased/38542-application-control-panel-in-settings-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add deploy strategies to the Auto DevOps settings
+merge_request: 19172
+author:
+type: added
diff --git a/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml b/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml
new file mode 100644
index 00000000000..fb4fbf80575
--- /dev/null
+++ b/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Label list page redesign
+merge_request: 18466
+author:
+type: changed
diff --git a/changelogs/unreleased/44267-improve-failed-jobs-tab.yml b/changelogs/unreleased/44267-improve-failed-jobs-tab.yml
new file mode 100644
index 00000000000..9743704e23d
--- /dev/null
+++ b/changelogs/unreleased/44267-improve-failed-jobs-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Improve Failed Jobs tab in the Pipeline detail page
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/44790-disabled-emails-logging.yml b/changelogs/unreleased/44790-disabled-emails-logging.yml
new file mode 100644
index 00000000000..90125dc0300
--- /dev/null
+++ b/changelogs/unreleased/44790-disabled-emails-logging.yml
@@ -0,0 +1,5 @@
+---
+title: Stop logging email information when emails are disabled
+merge_request: 18521
+author: Marc Shaw
+type: fixed
diff --git a/changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml b/changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml
new file mode 100644
index 00000000000..6974be07716
--- /dev/null
+++ b/changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml
@@ -0,0 +1,5 @@
+---
+title: Automatize Deploy Token creation for Auto Devops
+merge_request: 19507
+author:
+type: added
diff --git a/changelogs/unreleased/46648-timeout-searching-group-issues.yml b/changelogs/unreleased/46648-timeout-searching-group-issues.yml
new file mode 100644
index 00000000000..54401edf5cc
--- /dev/null
+++ b/changelogs/unreleased/46648-timeout-searching-group-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of group issues filtering on GitLab.com
+merge_request: 19429
+author:
+type: performance
diff --git a/changelogs/unreleased/46922-hashed-storage-single-project.yml b/changelogs/unreleased/46922-hashed-storage-single-project.yml
new file mode 100644
index 00000000000..c293238a5a4
--- /dev/null
+++ b/changelogs/unreleased/46922-hashed-storage-single-project.yml
@@ -0,0 +1,5 @@
+---
+title: 'Hashed Storage: migration rake task now can be executed to specific project'
+merge_request: 19268
+author:
+type: changed
diff --git a/changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml b/changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml
new file mode 100644
index 00000000000..010b1db5aac
--- /dev/null
+++ b/changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml
@@ -0,0 +1,5 @@
+---
+title: Use the default strings of timeago.js for timeago
+merge_request: 19350
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml b/changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml
new file mode 100644
index 00000000000..86680b6b117
--- /dev/null
+++ b/changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of LFS integrity check
+merge_request: 19494
+author:
+type: performance
diff --git a/changelogs/unreleased/feature-customizable-favicon.yml b/changelogs/unreleased/feature-customizable-favicon.yml
new file mode 100644
index 00000000000..0e5afc17c9e
--- /dev/null
+++ b/changelogs/unreleased/feature-customizable-favicon.yml
@@ -0,0 +1,5 @@
+---
+title: Allow changing the default favicon to a custom icon.
+merge_request: 14497
+author: Alexis Reigel
+type: added
diff --git a/changelogs/unreleased/ide-url-util-relative-url-fix.yml b/changelogs/unreleased/ide-url-util-relative-url-fix.yml
new file mode 100644
index 00000000000..9f0f4a0f7be
--- /dev/null
+++ b/changelogs/unreleased/ide-url-util-relative-url-fix.yml
@@ -0,0 +1,6 @@
+---
+title: Fixes Web IDE button on merge requests when GitLab is installed with relative
+ URL
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml b/changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml
new file mode 100644
index 00000000000..0789fc34f27
--- /dev/null
+++ b/changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Make CI job update entrypoint to work as keep-alive endpoint
+merge_request: 19543
+author:
+type: changed
diff --git a/changelogs/unreleased/issue_44230.yml b/changelogs/unreleased/issue_44230.yml
new file mode 100644
index 00000000000..2c6dba6c0fb
--- /dev/null
+++ b/changelogs/unreleased/issue_44230.yml
@@ -0,0 +1,5 @@
+---
+title: Apply notification settings level of groups to all child objects
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/n-plus-one-notification-recipients.yml b/changelogs/unreleased/n-plus-one-notification-recipients.yml
new file mode 100644
index 00000000000..91c31e4c930
--- /dev/null
+++ b/changelogs/unreleased/n-plus-one-notification-recipients.yml
@@ -0,0 +1,5 @@
+---
+title: Fix some sources of excessive query counts when calculating notification recipients
+merge_request:
+author:
+type: performance
diff --git a/config/initializers/disable_email_interceptor.rb b/config/initializers/disable_email_interceptor.rb
index c76a6b8b19f..e8770c8d460 100644
--- a/config/initializers/disable_email_interceptor.rb
+++ b/config/initializers/disable_email_interceptor.rb
@@ -1,2 +1,5 @@
# Interceptor in lib/disable_email_interceptor.rb
-ActionMailer::Base.register_interceptor(DisableEmailInterceptor) unless Gitlab.config.gitlab.email_enabled
+unless Gitlab.config.gitlab.email_enabled
+ ActionMailer::Base.register_interceptor(DisableEmailInterceptor)
+ ActionMailer::Base.logger = nil
+end
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index e9326653cbe..acbdf8de5a6 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -15,3 +15,5 @@ Mime::Type.register "video/ogg", :ogv
Mime::Type.unregister :json
Mime::Type.register 'application/json', :json, [LfsRequest::CONTENT_TYPE, 'application/json']
+
+Mime::Type.register 'image/x-icon', :ico
diff --git a/config/initializers/mini_magick.rb b/config/initializers/mini_magick.rb
new file mode 100644
index 00000000000..db0e7bbaaa3
--- /dev/null
+++ b/config/initializers/mini_magick.rb
@@ -0,0 +1,3 @@
+MiniMagick.configure do |config|
+ config.cli = :graphicsmagick
+end
diff --git a/config/locales/carrierwave.en.yml b/config/locales/carrierwave.en.yml
new file mode 100644
index 00000000000..12619226460
--- /dev/null
+++ b/config/locales/carrierwave.en.yml
@@ -0,0 +1,14 @@
+en:
+ errors:
+ messages:
+ carrierwave_processing_error: failed to be processed
+ carrierwave_integrity_error: is not of an allowed file type
+ carrierwave_download_error: could not be downloaded
+ extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
+ extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
+ content_type_whitelist_error: "You are not allowed to upload %{content_type} files"
+ content_type_blacklist_error: "You are not allowed to upload %{content_type} files"
+ rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?"
+ mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}"
+ min_size_error: "File size should be greater than %{min_size}"
+ max_size_error: "File size should be less than %{max_size}"
diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml
index 13732384953..c994bad7865 100644
--- a/config/prometheus/additional_metrics.yml
+++ b/config/prometheus/additional_metrics.yml
@@ -29,14 +29,14 @@
label: Pod average
unit: ms
- title: "HTTP Error Rate"
- y_label: "HTTP 500 Errors / Sec"
+ y_label: "HTTP Errors"
required_metrics:
- nginx_upstream_responses_total
weight: 1
queries:
- - query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))'
- label: HTTP Errors
- unit: "errors / sec"
+ - query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100'
+ label: 5xx Errors
+ unit: "%"
- group: Response metrics (HA Proxy)
priority: 10
metrics:
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 3cca1210e39..ff27ceb50dc 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -102,6 +102,7 @@ namespace :admin do
get :preview_sign_in
delete :logo
delete :header_logos
+ delete :favicon
end
end
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
index 6370645bcb9..6becadd57ae 100644
--- a/config/routes/uploads.rb
+++ b/config/routes/uploads.rb
@@ -17,7 +17,7 @@ scope path: :uploads do
# Appearance
get "-/system/:model/:mounted_as/:id/:filename",
to: "uploads#show",
- constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
+ constraints: { model: /appearance/, mounted_as: /logo|header_logo|favicon/, filename: /.+/ }
# Project markdown uploads
get ":namespace_id/:project_id/:secret/:filename",
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index 213c8bca639..51e69879c79 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -67,6 +67,10 @@ Sidekiq::Testing.inline! do
skip_disk_validation: true
}
+ if i % 2 == 0
+ params[:storage_version] = Project::LATEST_STORAGE_VERSION
+ end
+
project = Projects::CreateService.new(User.first, params).execute
# Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
# hook won't run until after the fixture is loaded. That is too late
diff --git a/db/fixtures/development/08_settings.rb b/db/fixtures/development/08_settings.rb
new file mode 100644
index 00000000000..141465c06cf
--- /dev/null
+++ b/db/fixtures/development/08_settings.rb
@@ -0,0 +1,7 @@
+# We want to enable hashed storage for every new project in development
+# Details https://gitlab.com/gitlab-org/gitlab-ce/issues/46241
+Gitlab::Seeder.quiet do
+ ApplicationSetting.create_from_defaults unless ApplicationSetting.current_without_cache
+ ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true)
+ print '.'
+end
diff --git a/db/migrate/20170925184228_add_favicon_to_appearances.rb b/db/migrate/20170925184228_add_favicon_to_appearances.rb
new file mode 100644
index 00000000000..65083733afb
--- /dev/null
+++ b/db/migrate/20170925184228_add_favicon_to_appearances.rb
@@ -0,0 +1,7 @@
+class AddFaviconToAppearances < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :appearances, :favicon, :string
+ end
+end
diff --git a/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb b/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb
new file mode 100644
index 00000000000..6f50d428965
--- /dev/null
+++ b/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddDeployStrategyToProjectAutoDevops < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :project_auto_devops, :deploy_strategy, :integer, default: 0, allow_null: false
+ end
+
+ def down
+ remove_column :project_auto_devops, :deploy_strategy
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f6fb1c92f8d..d1446af0a2e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -38,6 +38,7 @@ ActiveRecord::Schema.define(version: 20180603190921) do
t.integer "cached_markdown_version"
t.text "new_project_guidelines"
t.text "new_project_guidelines_html"
+ t.string "favicon"
end
create_table "application_setting_terms", force: :cascade do |t|
@@ -1493,6 +1494,7 @@ ActiveRecord::Schema.define(version: 20180603190921) do
t.datetime_with_timezone "updated_at", null: false
t.boolean "enabled"
t.string "domain"
+ t.integer "deploy_strategy", default: 0, null: false
end
add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
diff --git a/doc/administration/index.md b/doc/administration/index.md
index df935095e61..0e65f9a9963 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -49,6 +49,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
#### Customizing GitLab's appearance
- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers.
+- [Favicon](../customization/favicon.md): Change the default favicon to your own logo.
- [Branded login page](../customization/branded_login_page.md): Customize the login page with your own logo, title, and description.
- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page.
- ["New Project" page](../customization/new_project_page.md): Customize the text to be displayed on the page that opens whenever your users create a new project.
diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md
index cfd601b8866..7ad38abe4f5 100644
--- a/doc/administration/raketasks/storage.md
+++ b/doc/administration/raketasks/storage.md
@@ -17,13 +17,21 @@ This task will schedule all your existing projects and attachments associated wi
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:migrate_to_hashed
+sudo gitlab-rake gitlab:storage:migrate_to_hashed
```
**Source Installation**
```bash
-rake gitlab:storage:migrate_to_hashed
+sudo -u git -H bundle exec rake gitlab:storage:migrate_to_hashed RAILS_ENV=production
+```
+
+They both also accept a range as environment variable:
+
+```bash
+# to migrate any non migrated project from ID 20 to 50.
+export ID_FROM=20
+export ID_TO=50
```
You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen.
@@ -44,13 +52,13 @@ To have a simple summary of projects using **Legacy** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:legacy_projects
+sudo gitlab-rake gitlab:storage:legacy_projects
```
**Source Installation**
```bash
-rake gitlab:storage:legacy_projects
+sudo -u git -H bundle exec rake gitlab:storage:legacy_projects RAILS_ENV=production
```
------
@@ -60,13 +68,13 @@ To list projects using **Legacy** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:list_legacy_projects
+sudo gitlab-rake gitlab:storage:list_legacy_projects
```
**Source Installation**
```bash
-rake gitlab:storage:list_legacy_projects
+sudo -u git -H bundle exec rake gitlab:storage:list_legacy_projects RAILS_ENV=production
```
@@ -77,13 +85,13 @@ To have a simple summary of projects using **Hashed** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:hashed_projects
+sudo gitlab-rake gitlab:storage:hashed_projects
```
**Source Installation**
```bash
-rake gitlab:storage:hashed_projects
+sudo -u git -H bundle exec rake gitlab:storage:hashed_projects RAILS_ENV=production
```
------
@@ -93,14 +101,13 @@ To list projects using **Hashed** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:list_hashed_projects
+sudo gitlab-rake gitlab:storage:list_hashed_projects
```
**Source Installation**
```bash
-rake gitlab:storage:list_hashed_projects
-
+sudo -u git -H bundle exec rake gitlab:storage:list_hashed_projects RAILS_ENV=production
```
## List attachments on Legacy storage
@@ -110,13 +117,13 @@ To have a simple summary of project attachments using **Legacy** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:legacy_attachments
+sudo gitlab-rake gitlab:storage:legacy_attachments
```
**Source Installation**
```bash
-rake gitlab:storage:legacy_attachments
+sudo -u git -H bundle exec rake gitlab:storage:legacy_attachments RAILS_ENV=production
```
------
@@ -126,13 +133,13 @@ To list project attachments using **Legacy** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:list_legacy_attachments
+sudo gitlab-rake gitlab:storage:list_legacy_attachments
```
**Source Installation**
```bash
-rake gitlab:storage:list_legacy_attachments
+sudo -u git -H bundle exec rake gitlab:storage:list_legacy_attachments RAILS_ENV=production
```
## List attachments on Hashed storage
@@ -142,13 +149,13 @@ To have a simple summary of project attachments using **Hashed** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:hashed_attachments
+sudo gitlab-rake gitlab:storage:hashed_attachments
```
**Source Installation**
```bash
-rake gitlab:storage:hashed_attachments
+sudo -u git -H bundle exec rake gitlab:storage:hashed_attachments RAILS_ENV=production
```
------
@@ -158,13 +165,13 @@ To list project attachments using **Hashed** storage:
**Omnibus Installation**
```bash
-gitlab-rake gitlab:storage:list_hashed_attachments
+sudo gitlab-rake gitlab:storage:list_hashed_attachments
```
**Source Installation**
```bash
-rake gitlab:storage:list_hashed_attachments
+sudo -u git -H bundle exec rake gitlab:storage:list_hashed_attachments RAILS_ENV=production
```
[storage-types]: ../repository_storage_types.md
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 36fd8affa5b..8ea2e0a81dc 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -114,7 +114,7 @@ Let's now see how that information is exposed within GitLab.
## Viewing the current status of an environment
-The environment list under your project's **Pipelines âž” Environments**, is
+The environment list under your project's **Operations > Environments**, is
where you can find information of the last deployment status of an environment.
Here's how the Environments page looks so far.
@@ -167,7 +167,7 @@ that works.
You can't control everything, so sometimes things go wrong. When that unfortunate
time comes GitLab has you covered. Simply by clicking the **Rollback** button
that can be found in the deployments page
-(**Pipelines âž” Environments âž” `environment name`**) you can relaunch the
+(**Operations > Environments > `environment name`**) you can relaunch the
job with the commit associated with it.
>**Note:**
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index de60cd27cd1..aa31e172641 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -19,7 +19,9 @@ There's also a collection of repositories with [example projects](https://gitlab
- [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md)
- **Ruby**: [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
- **Python**: [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
-- **Java**: [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
+- **Java**:
+ - [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md)
+ - [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
- **Scala**: [Test a Scala application](test-scala-application.md)
- **Clojure**: [Test a Clojure application](test-clojure-application.md)
- **Elixir**:
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png
new file mode 100644
index 00000000000..5b5d91ec07a
--- /dev/null
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png
Binary files differ
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png
new file mode 100644
index 00000000000..f3761632556
--- /dev/null
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png
Binary files differ
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
new file mode 100644
index 00000000000..b88761be56b
--- /dev/null
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
@@ -0,0 +1,142 @@
+---
+author: Dylan Griffith
+author_gitlab: DylanGriffith
+level: intermediary
+article_type: tutorial
+date: 2018-06-07
+description: "Continuous Deployment of a Spring Boot application to Cloud Foundry with GitLab CI/CD"
+---
+
+# Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD
+
+## Introduction
+
+In this article, we'll demonstrate how to deploy a [Spring
+Boot](https://projects.spring.io/spring-boot/) application to [Cloud
+Foundry (CF)](https://www.cloudfoundry.org/) with GitLab CI/CD using the [Continuous
+Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-deployment)
+method.
+
+All the code for this project can be found in this [GitLab
+repo](https://gitlab.com/gitlab-examples/spring-gitlab-cf-deploy-demo).
+
+In case you're interested in deploying Spring Boot applications to Kubernetes
+using GitLab CI/CD, read through the blog post [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/).
+
+## Requirements
+
+_We assume you are familiar with Java, GitLab, Cloud Foundry, and GitLab CI/CD._
+
+To follow along with this tutorial you will need the following:
+
+- An account on [Pivotal Web Services (PWS)](https://run.pivotal.io/) or any
+ other Cloud Foundry instance
+- An account on GitLab
+
+NOTE: **Note:**
+You will need to replace the `api.run.pivotal.io` URL in the all below
+commands with the [API
+URL](https://docs.cloudfoundry.org/running/cf-api-endpoint.html) of your CF
+instance if you're not deploying to PWS.
+
+## Create your project
+
+To create your Spring Boot application you can use the Spring template in
+GitLab when creating a new project:
+
+![New Project From Template](img/create_from_template.png)
+
+## Configure the deployment to Cloud Foundry
+
+To deploy to Cloud Foundry we need to add a `manifest.yml` file. This
+is the configuration for the CF CLI we will use to deploy the application. We
+will create this in the root directory of our project with the following
+content:
+
+```yaml
+---
+applications:
+- name: gitlab-hello-world
+ random-route: true
+ memory: 1G
+ path: target/demo-0.0.1-SNAPSHOT.jar
+```
+
+## Configure GitLab CI/CD to deploy your application
+
+Now we need to add the the GitLab CI/CD configuration file
+([`.gitlab-ci.yml`](../../yaml/README.md)) to our
+project's root. This is how GitLab figures out what commands need to be run whenever
+code is pushed to our repository. We will add the following `.gitlab-ci.yml`
+file to the root directory of the repository, GitLab will detect it
+automatically and run the steps defined once we push our code:
+
+```yaml
+image: java:8
+
+stages:
+ - build
+ - deploy
+
+build:
+ stage: build
+ script: ./mvnw package
+ artifacts:
+ paths:
+ - target/demo-0.0.1-SNAPSHOT.jar
+
+production:
+ stage: deploy
+ script:
+ - curl --location "https://cli.run.pivotal.io/stable?release=linux64-binary&source=github" | tar zx
+ - ./cf login -u $CF_USERNAME -p $CF_PASSWORD -a api.run.pivotal.io
+ - ./cf push
+ only:
+ - master
+```
+
+We've used the `java:8` [docker
+image](../../docker/using_docker_images.md) to build
+our application as it provides the up-to-date Java 8 JDK on [Docker
+Hub](https://hub.docker.com/). We've also added the [`only`
+clause](../../yaml/README.md#only-and-except-simplified)
+to ensure our deployments only happen when we push to the master branch.
+
+Now, since the steps defined in `.gitlab-ci.yml` require credentials to login
+to CF, you'll need to add your CF credentials as [environment
+variables](../../variables/README.md#predefined-variables-environment-variables)
+on GitLab CI/CD. To set the environment variables, navigate to your project's
+**Settings > CI/CD** and expand **Secret Variables**. Name the variables
+`CF_USERNAME` and `CF_PASSWORD` and set them to the correct values.
+
+![Secret Variable Settings in GitLab](img/cloud_foundry_secret_variables.png)
+
+Once set up, GitLab CI/CD will deploy your app to CF at every push to your
+repository's deafult branch. To see the build logs or watch your builds running
+live, navigate to **CI/CD > Pipelines**.
+
+CAUTION: **Caution:**
+It is considered best practice for security to create a separate deploy
+user for your application and add its credentials to GitLab instead of using
+a developer's credentials.
+
+To start a manual deployment in GitLab go to **CI/CD > Pipelines** then click
+on **Run Pipeline**. Once the app is finished deploying it will display the URL
+of your application in the logs for the `production` job like:
+
+```shell
+requested state: started
+instances: 1/1
+usage: 1G x 1 instances
+urls: gitlab-hello-world-undissembling-hotchpot.cfapps.io
+last uploaded: Mon Nov 6 10:02:25 UTC 2017
+stack: cflinuxfs2
+buildpack: client-certificate-mapper=1.2.0_RELEASE container-security-provider=1.8.0_RELEASE java-buildpack=v4.5-offline-https://github.com/cloudfoundry/java-buildpack.git#ffeefb9 java-main java-opts jvmkill-agent=1.10.0_RELEASE open-jdk-like-jre=1.8.0_1...
+
+ state since cpu memory disk details
+#0 running 2017-11-06 09:03:22 PM 120.4% 291.9M of 1G 137.6M of 1G
+```
+
+You can then visit your deployed application (for this example,
+https://gitlab-hello-world-undissembling-hotchpot.cfapps.io/) and you should
+see the "Spring is here!" message.
diff --git a/doc/customization/favicon.md b/doc/customization/favicon.md
new file mode 100644
index 00000000000..45a18159b5e
--- /dev/null
+++ b/doc/customization/favicon.md
@@ -0,0 +1,16 @@
+# Changing the favicon
+
+> [Introduced][ce-14497] in GitLab 11.0.
+
+[ce-14497]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14497
+
+Navigate to the **Admin** area and go to the **Appearance** page.
+
+Upload the custom favicon (**Favicon**) in the section **Favicon**.
+
+![appearance](favicon/appearance.png)
+
+After saving the page, the new favicon will be shown in the browser. The main
+favicon as well as the CI status icons will show the custom icon:
+
+![custom_favicon](favicon/custom_favicon.png)
diff --git a/doc/customization/favicon/appearance.png b/doc/customization/favicon/appearance.png
new file mode 100644
index 00000000000..6c41a05fc1f
--- /dev/null
+++ b/doc/customization/favicon/appearance.png
Binary files differ
diff --git a/doc/customization/favicon/custom_favicon.png b/doc/customization/favicon/custom_favicon.png
new file mode 100644
index 00000000000..fa1b8827a36
--- /dev/null
+++ b/doc/customization/favicon/custom_favicon.png
Binary files differ
diff --git a/doc/topics/autodevops/img/autodevops_domain_variables.png b/doc/topics/autodevops/img/autodevops_domain_variables.png
new file mode 100644
index 00000000000..b6f8864796f
--- /dev/null
+++ b/doc/topics/autodevops/img/autodevops_domain_variables.png
Binary files differ
diff --git a/doc/topics/autodevops/img/autodevops_multiple_clusters.png b/doc/topics/autodevops/img/autodevops_multiple_clusters.png
new file mode 100644
index 00000000000..f4d101ca921
--- /dev/null
+++ b/doc/topics/autodevops/img/autodevops_multiple_clusters.png
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 478b9c9c0c6..e2edee42717 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -41,6 +41,7 @@ project in an easy and automatic way:
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 License Management](#auto-license-management)
1. [Auto Container Scanning](#auto-container-scanning)
1. [Auto Review Apps](#auto-review-apps)
1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast)
@@ -62,7 +63,7 @@ Auto DevOps provides great defaults for all the stages; you can, however,
For an overview on the creation of Auto DevOps, read the blog post [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/).
-## Prerequisites
+## Requirements
TIP: **Tip:**
For self-hosted installations, the easiest way to make use of Auto DevOps is to
@@ -112,25 +113,26 @@ NOTE: **Note:**
If you do not have Kubernetes or Prometheus installed, then Auto Review Apps,
Auto Deploy, and Auto Monitoring will be silently skipped.
-### Auto DevOps base domain
+## Auto DevOps base domain
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
-[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`.
+Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It can be defined
+in three places:
-A wildcard DNS A record matching the base domain is required, for example,
+- either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops)
+- or in instance-wide settings in the **admin area > Settings** under the "Continuous Integration and Delivery" section
+- or at the project or group level as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters))
+
+A wildcard DNS A record matching the base domain(s) is required, for example,
given a base domain of `example.com`, you'd need a DNS entry like:
```
*.example.com 3600 A 1.2.3.4
```
-where `example.com` is the domain name under which the deployed apps will be served,
+In this case, `example.com` is the domain name under which the deployed apps will be served,
and `1.2.3.4` is the IP address of your load balancer; generally NGINX
-([see prerequisites](#prerequisites)). How to set up the DNS record is beyond
+([see requirements](#requirements)). How to set up the DNS record is beyond
the scope of this document; you should check with your DNS provider.
Alternatively you can use free public services like [xip.io](http://xip.io) or
@@ -146,6 +148,56 @@ If GitLab is installed using the [GitLab Omnibus Helm Chart], there are two
options: provide a static IP, or have one assigned. For more information see the
relevant docs on the [network prerequisites](../../install/kubernetes/gitlab_omnibus.md#networking-prerequisites).
+## Using multiple Kubernetes clusters **[PREMIUM]**
+
+When using Auto DevOps, you may want to deploy different environments to
+different Kubernetes clusters. This is possible due to the 1:1 connection that
+[exists between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters).
+
+In the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml)
+(used behind the scenes by Auto DevOps), there are currently 3 defined environment names that you need to know:
+
+- `review/` (every environment starting with `review/`)
+- `staging`
+- `production`
+
+Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so
+except for the environment scope, they would also need to have a different
+domain they would be deployed to. This is why you need to define a separate
+`AUTO_DEVOPS_DOMAIN` variable for all the above
+[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-variables).
+
+The following table is an example of how the three different clusters would
+be configured.
+
+| Cluster name | Cluster environment scope | `AUTO_DEVOPS_DOMAIN` variable value | Variable environment scope | Notes |
+| ------------ | -------------- | ----------------------------- | ------------- | ------ |
+| review | `review/*` | `review.example.com` | `review/*` | The review cluster which will run all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, which means it will be used by every environment name starting with `review/`. |
+| staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which will run the deployments of the staging environments. You need to [enable it first](#deploy-policy-for-staging-and-production-environments). |
+| production | `production` | `example.com` | `production` | The production cluster which will run the deployments of the production environment. You can use [incremental rollouts](#incremental-rollout-to-production). |
+
+To add a different cluster for each environment:
+
+1. Navigate to your project's **Operations > Kubernetes** and create the Kubernetes clusters
+ with their respective environment scope as described from the table above.
+
+ ![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png)
+
+1. After the clusters are created, navigate to each one and install Helm Tiller
+ and Ingress.
+1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the
+ specified Auto DevOps domains.
+1. Navigate to your project's **Settings > CI/CD > Variables** and add
+ the `AUTO_DEVOPS_DOMAIN` variables with their respective environment
+ scope.
+
+ ![Auto DevOps domain variables](img/autodevops_domain_variables.png)
+
+Now that all is configured, you can test your setup by creating a merge request
+and verifying that your app is deployed as a review app in the Kubernetes
+cluster with the `review/*` environment scope. Similarly, you can check the
+other environments.
+
## Quick start
If you are using GitLab.com, see our [quick start guide](quick_start_guide.md)
@@ -154,13 +206,13 @@ Google Cloud.
## Enabling Auto DevOps
-If you haven't done already, read the [prerequisites](#prerequisites) to make
+If you haven't done already, read the [requirements](#requirements) to make
full use of Auto DevOps. If this is your fist time, we recommend you follow the
-[quick start guide](#quick-start).
+[quick start guide](quick_start_guide.md).
To enable Auto DevOps to your project:
-1. Check that your project doesn't have a `.gitlab-ci.yml`, and remove it otherwise
+1. Check that your project doesn't have a `.gitlab-ci.yml`, or remove it otherwise
1. Go to your project's **Settings > CI/CD > General pipelines settings** and
find the Auto DevOps section
1. Select "Enable Auto DevOps"
@@ -230,7 +282,7 @@ In GitLab Starter, differences between the source and
target branches are also
[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html).
-### Auto SAST
+### Auto SAST **[ULTIMATE]**
> Introduced in [GitLab Ultimate][ee] 10.3.
@@ -241,9 +293,9 @@ 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/sast.html).
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/sast.html).
-### Auto Dependency Scanning
+### Auto Dependency Scanning **[ULTIMATE]**
> Introduced in [GitLab Ultimate][ee] 10.7.
@@ -254,7 +306,20 @@ 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).
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dependency_scanning.html).
+
+### Auto License Management **[ULTIMATE]**
+
+> Introduced in [GitLab Ultimate][ee] 11.0.
+
+License Management uses the
+[License Management Docker image](https://gitlab.com/gitlab-org/security-products/license_management)
+to search the project dependencies for their license. Once the
+report is created, it's uploaded as an artifact which you can later download and
+check out.
+
+In GitLab Ultimate, any licenses are also
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/license_management.html).
### Auto Container Scanning
@@ -267,13 +332,13 @@ 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/container_scanning.html).
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/container_scanning.html).
### Auto Review Apps
NOTE: **Note:**
This is an optional step, since many projects do not have a Kubernetes cluster
-available. If the [prerequisites](#prerequisites) are not met, the job will
+available. If the [requirements](#requirements) are not met, the job will
silently be skipped.
CAUTION: **Caution:**
@@ -295,7 +360,7 @@ up in the merge request widget for easy discovery. When the branch is deleted,
for example after the merge request is merged, the Review App will automatically
be deleted.
-### Auto DAST
+### Auto DAST **[ULTIMATE]**
> Introduced in [GitLab Ultimate][ee] 10.4.
@@ -306,9 +371,9 @@ 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/dast.html).
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dast.html).
-### Auto Browser Performance Testing
+### Auto Browser Performance Testing **[PREMIUM]**
> Introduced in [GitLab Premium][ee] 10.4.
@@ -320,13 +385,14 @@ Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://h
/direction
```
-In GitLab Premium, performance differences between the source and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
+In GitLab Premium, performance differences between the source
+and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html).
### Auto Deploy
NOTE: **Note:**
This is an optional step, since many projects do not have a Kubernetes cluster
-available. If the [prerequisites](#prerequisites) are not met, the job will
+available. If the [requirements](#requirements) are not met, the job will
silently be skipped.
CAUTION: **Caution:**
@@ -360,10 +426,19 @@ no longer be valid as soon as the deployment job finishes. This means that
Kubernetes can run the application, but in case it should be restarted or
executed somewhere else, it cannot be accessed again.
+> [Introduced][ce-19507] in GitLab 11.0.
+
+For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md###gitlab-deploy-token)
+will be automatically created, when Auto DevOps is enabled and the Auto DevOps settings are saved. This Deploy Token
+can be used for permanent access to the registry.
+
+Note: **Note**
+When the GitLab Deploy Token has been manually revoked, it won't be automatically created.
+
### Auto Monitoring
NOTE: **Note:**
-Check the [prerequisites](#prerequisites) for Auto Monitoring to make this stage
+Check the [requirements](#requirements) for Auto Monitoring to make this stage
work.
Once your application is deployed, Auto Monitoring makes it possible to monitor
@@ -437,7 +512,7 @@ repo or by specifying a project variable:
file in it, Auto DevOps will detect the chart and use it instead of the [default
one](https://gitlab.com/charts/charts.gitlab.io/tree/master/charts/auto-deploy-app).
This can be a great way to control exactly how your application is deployed.
-- **Project variable** - Create a [variable](../../ci/variables/README.md#variables)
+- **Project variable** - Create a [project variable](../../ci/variables/README.md#secret-variables)
`AUTO_DEVOPS_CHART` with the URL of a custom chart to use.
### Customizing `.gitlab-ci.yml`
@@ -493,22 +568,23 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. |
| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. |
| `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` |
+| `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.|
+| `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).|
| `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). |
| `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). |
| `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. |
| `TEST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `test` job. If the variable is present, the job will not be created. |
-| `CODE_QUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `code_quality` job. If the variable is present, the job will not be created. |
-| `LICENSE_MANAGEMENT_DISABLED` | From GitLab 11.0, this variable can be used to disable the `license_management` job. If the variable is present, the job will not be created. |
+| `CODEQUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `codequality` job. If the variable is present, the job will not be created. |
| `SAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. |
| `DEPENDENCY_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dependency_scanning` job. If the variable is present, the job will not be created. |
-| `CONTAINER_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `container_scanning` job. If the variable is present, the job will not be created. |
+| `CONTAINER_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast:container` job. If the variable is present, the job will not be created. |
| `REVIEW_DISABLED` | From GitLab 11.0, this variable can be used to disable the `review` and the manual `review:stop` job. If the variable is present, these jobs will not be created. |
| `DAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dast` job. If the variable is present, the job will not be created. |
| `PERFORMANCE_DISABLED` | From GitLab 11.0, this variable can be used to disable the `performance` job. If the variable is present, the job will not be created. |
TIP: **Tip:**
Set up the replica variables using a
-[project variable](../../ci/variables/README.md#variables)
+[project variable](../../ci/variables/README.md#secret-variables)
and scale your application by just redeploying it!
CAUTION: **Caution:**
@@ -583,7 +659,7 @@ staging environment and deploy to production manually. For this scenario, the
`STAGING_ENABLED` environment variable was introduced.
If `STAGING_ENABLED` is defined in your project (e.g., set `STAGING_ENABLED` to
-`1` as a variable), then the application will be automatically deployed
+`1` as a secret variable), then the application will be automatically deployed
to a `staging` environment, and a `production_manual` job will be created for
you when you're ready to manually deploy to production.
@@ -596,7 +672,7 @@ A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployment
before any changes are deployed to production.
If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to
-`1` as a variable) then two manual jobs will be created:
+`1` as a secret variable) then two manual jobs will be created:
- `canary` which will deploy the application to the canary environment
- `production_manual` which is to be used by you when you're ready to manually
@@ -612,7 +688,7 @@ This will allow you to first check how the app is behaving, and later manually
increasing the rollout up to 100%.
If `INCREMENTAL_ROLLOUT_ENABLED` is defined in your project (e.g., set
-`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a variable), then instead of the
+`INCREMENTAL_ROLLOUT_ENABLED` to `1` as a secret variable), then instead of the
standard `production` job, 4 different
[manual jobs](../../ci/pipelines.md#manual-actions-from-the-pipeline-graph)
will be created:
@@ -742,3 +818,4 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/
[Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml
[GitLab Omnibus Helm Chart]: ../../install/kubernetes/gitlab_omnibus.md
[ee]: https://about.gitlab.com/products/
+[ce-19507]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19507
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 0c1cd113686..d054561d5f3 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -60,6 +60,7 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md).
| Setting | GitLab.com | Default |
| ----------- | ----------------- | ------------- |
| Artifacts maximum size | 1G | 100M |
+| Artifacts [expiry time](../../ci/yaml/README.md#artifacts-expire_in) | kept forever | deleted after 30 days unless otherwise specified |
## Repository size limit
diff --git a/doc/user/project/clusters/eks_and_gitlab/index.md b/doc/user/project/clusters/eks_and_gitlab/index.md
index bd4d15dddcb..2d8fdf0d1da 100644
--- a/doc/user/project/clusters/eks_and_gitlab/index.md
+++ b/doc/user/project/clusters/eks_and_gitlab/index.md
@@ -38,7 +38,7 @@ Give the project a name, and then select `Create project`.
## Connecting the EKS cluster
-From the left side bar, hover over `CI/CD` and select `Kubernetes`, then click on `Add Kubernetes cluster`, and finally `Add an existing Kubernetes cluster`.
+From the left side bar, hover over `Operations` and select `Kubernetes`, then click on `Add Kubernetes cluster`, and finally `Add an existing Kubernetes cluster`.
A few details from the EKS cluster will be required to connect it to GitLab.
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index e44ac6a3d01..1e909e9f5f7 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -39,7 +39,7 @@ Before proceeding, make sure the following requirements are met:
If all of the above requirements are met, you can proceed to create and add a
new Kubernetes cluster that will be hosted on GKE to your project:
-1. Navigate to your project's **CI/CD > Kubernetes** page.
+1. Navigate to your project's **Operations > Kubernetes** page.
1. Click on **Add Kubernetes cluster**.
1. Click on **Create with GKE**.
1. Connect your Google account if you haven't done already by clicking the
@@ -70,7 +70,7 @@ You need Maintainer [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. Navigate to your project's **Operations > Kubernetes** page.
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.
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index cad85881c4d..fcd6192e82f 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -29,7 +29,9 @@ The following aspects of a project are imported:
* Regular issue and pull request comments
References to pull requests and issues are preserved (GitLab.com & 8.7+), and
-each imported repository defaults to `private` but [can be made public](../settings/index.md#sharing-and-permissions), as needed.
+each imported repository maintains visibility level unless that [visibility
+level is restricted](../../../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects),
+in which case it defaults to the default project visibility.
## How it works
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index fa7e504c4aa..f687027e8c8 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -30,7 +30,7 @@ GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cl
Once you have a connected Kubernetes cluster with Helm installed, deploying a managed Prometheus is as easy as a single click.
-1. Go to the `CI/CD > Kubernetes` page, to view your connected clusters
+1. Go to the `Operations > Kubernetes` page, to view your connected clusters
1. Select the cluster you would like to deploy Prometheus to
1. Click the **Install** button to deploy Prometheus to the cluster
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
index 590b1c4275a..a1db79538a4 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
@@ -14,7 +14,7 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI
| ---- | ----- |
| Throughput (req/sec) | sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code) |
| Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) |
-| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
+| HTTP Error Rate (%) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100 |
## Configuring NGINX ingress monitoring
diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md
index 50ec99be48b..6c1378560ef 100644
--- a/doc/user/reserved_names.md
+++ b/doc/user/reserved_names.md
@@ -58,7 +58,7 @@ Currently the following names are reserved as top level groups:
- dashboard
- deploy.html
- explore
-- favicon.ico
+- favicon.png
- groups
- header_logo_dark.png
- header_logo_light.png
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index fc592b99860..edb0c6bdc30 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -34,9 +34,14 @@ anything that is set at Global Settings.
![notification settings](img/notification_group_settings.png)
-Group Settings are taking precedence over Global Settings but are on a level below Project Settings.
+Group Settings are taking precedence over Global Settings but are on a level below Project or Subgroup Settings:
+
+```
+Group < Subgroup < Project
+```
+
This means that you can set a different level of notifications per group while still being able
-to have a finer level setting per project.
+to have a finer level setting per project or subgroup.
Organization like this is suitable for users that belong to different groups but don't have the
same need for being notified for every group they are member of.
These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown.
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index 35ac0b4cbca..61eb88d3331 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -59,6 +59,11 @@ module API
def max_artifacts_size
Gitlab::CurrentSettings.max_artifacts_size.megabytes.to_i
end
+
+ def job_forbidden!(job, reason)
+ header 'Job-Status', job.status
+ forbidden!(reason)
+ end
end
end
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index db502697a19..dc102259ca8 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -125,7 +125,7 @@ module API
end
put '/:id' do
job = authenticate_job!
- forbidden!('Job is not running') unless job.running?
+ job_forbidden!(job, 'Job is not running') unless job.running?
job.trace.set(params[:trace]) if params[:trace]
@@ -133,6 +133,8 @@ module API
project: job.project.full_path)
case params[:state].to_s
+ when 'running'
+ job.touch if job.needs_touch?
when 'success'
job.success!
when 'failed'
@@ -152,7 +154,7 @@ module API
end
patch '/:id/trace' do
job = authenticate_job!
- forbidden!('Job is not running') unless job.running?
+ job_forbidden!(job, 'Job is not running') unless job.running?
error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
content_range = request.headers['Content-Range']
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index d769a3ee7b0..e287aa1e392 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -29,10 +29,10 @@ module Backup
raise Backup::Error, 'Backup failed'
end
- run_pipeline!([%W(tar --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ run_pipeline!([%W(#{tar} --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
FileUtils.rm_rf(@backup_files_dir)
else
- run_pipeline!([%W(tar --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ run_pipeline!([%W(#{tar} --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600])
end
end
@@ -43,7 +43,12 @@ module Backup
end
def tar
- system(*%w[gtar --version], out: '/dev/null') ? 'gtar' : 'tar'
+ if system(*%w[gtar --version], out: '/dev/null')
+ # It looks like we can get GNU tar by running 'gtar'
+ 'gtar'
+ else
+ 'tar'
+ end
end
def backup_existing_files_dir
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index a129746e2c6..b9a148f35bf 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -33,6 +33,7 @@ module Gitlab
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
VERSION = File.read(root.join("VERSION")).strip.freeze
+ INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze
def self.com?
# Check `gl_subdomain?` as well to keep parity with gitlab.com
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 6cf7aa1bf0d..3cf35f499cd 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -24,7 +24,22 @@ module Gitlab
private
def ensure_application_settings!
+ cached_application_settings || uncached_application_settings
+ end
+
+ def cached_application_settings
return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true'
+
+ begin
+ ::ApplicationSetting.cached
+ rescue
+ # In case Redis isn't running
+ # or the Redis UNIX socket file is not available
+ # or the DB is not running (we use migrations in the cache key)
+ end
+ end
+
+ def uncached_application_settings
return fake_application_settings unless connect_to_db?
current_settings = ::ApplicationSetting.current
diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb
new file mode 100644
index 00000000000..451c9daf761
--- /dev/null
+++ b/lib/gitlab/favicon.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ class Favicon
+ class << self
+ def main
+ return appearance_favicon.favicon_main.url if appearance_favicon.exists?
+
+ image_name =
+ if Gitlab::Utils.to_boolean(ENV['CANARY'])
+ 'favicon-yellow.png'
+ elsif Rails.env.development?
+ 'favicon-blue.png'
+ else
+ 'favicon.png'
+ end
+
+ ActionController::Base.helpers.image_path(image_name)
+ end
+
+ def status_overlay(status_name)
+ path = File.join(
+ 'ci_favicons',
+ "#{status_name}.png"
+ )
+
+ ActionController::Base.helpers.image_path(path)
+ end
+
+ def available_status_names
+ @available_status_names ||= begin
+ Dir.glob(Rails.root.join('app', 'assets', 'images', 'ci_favicons', '*.png'))
+ .map { |file| File.basename(file, '.png') }
+ .sort
+ end
+ end
+
+ private
+
+ def appearance
+ RequestStore.store[:appearance] ||= (Appearance.current || Appearance.new)
+ end
+
+ def appearance_favicon
+ appearance.favicon
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
index 320b2ad007b..f3cc388ea41 100644
--- a/lib/gitlab/git/lfs_changes.rb
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -30,7 +30,7 @@ module Gitlab
def git_new_pointers(object_limit, not_in)
@new_pointers ||= begin
- rev_list.new_objects(not_in: not_in, require_path: true) do |object_ids|
+ rev_list.new_objects(rev_list_params(not_in: not_in)) do |object_ids|
object_ids = object_ids.take(object_limit) if object_limit
Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
@@ -39,9 +39,12 @@ module Gitlab
end
def git_all_pointers
- params = { options: ["--filter=blob:limit=#{Gitlab::Git::Blob::LFS_POINTER_MAX_SIZE}"], require_path: true }
+ params = {}
+ if rev_list_supports_new_options?
+ params[:options] = ["--filter=blob:limit=#{Gitlab::Git::Blob::LFS_POINTER_MAX_SIZE}"]
+ end
- rev_list.all_objects(params) do |object_ids|
+ rev_list.all_objects(rev_list_params(params)) do |object_ids|
Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
end
end
@@ -49,6 +52,23 @@ module Gitlab
def rev_list
Gitlab::Git::RevList.new(@repository, newrev: @newrev)
end
+
+ # We're passing the `--in-commit-order` arg to ensure we don't wait
+ # for git to traverse all commits before returning pointers.
+ # This is required in order to improve the performance of LFS integrity check
+ def rev_list_params(params = {})
+ params[:options] ||= []
+ params[:options] << "--in-commit-order" if rev_list_supports_new_options?
+ params[:require_path] = true
+
+ params
+ end
+
+ def rev_list_supports_new_options?
+ return @option_supported if defined?(@option_supported)
+
+ @option_supported = Gitlab::Git.version >= Gitlab::VersionInfo.parse('2.16.0')
+ end
end
end
end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 33b641a090b..4e661eceffb 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -27,9 +27,10 @@ module Gitlab
#
# When given a block it will yield objects as a lazy enumerator so
# the caller can limit work done instead of processing megabytes of data
- def new_objects(require_path: nil, not_in: nil, &lazy_block)
+ def new_objects(options: [], require_path: nil, not_in: nil, &lazy_block)
opts = {
including: newrev,
+ options: options,
excluding: not_in.nil? ? :all : not_in,
require_path: require_path
}
diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb
new file mode 100644
index 00000000000..9251ed654cd
--- /dev/null
+++ b/lib/gitlab/hashed_storage/migrator.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module HashedStorage
+ # Hashed Storage Migrator
+ #
+ # This is responsible for scheduling and flagging projects
+ # to be migrated from Legacy to Hashed storage, either one by one or in bulk.
+ class Migrator
+ BATCH_SIZE = 100
+
+ # Schedule a range of projects to be bulk migrated with #bulk_migrate asynchronously
+ #
+ # @param [Object] start first project id for the range
+ # @param [Object] finish last project id for the range
+ def bulk_schedule(start, finish)
+ StorageMigratorWorker.perform_async(start, finish)
+ end
+
+ # Start migration of projects from specified range
+ #
+ # Flagging a project to be migrated is a synchronous action,
+ # but the migration runs through async jobs
+ #
+ # @param [Object] start first project id for the range
+ # @param [Object] finish last project id for the range
+ def bulk_migrate(start, finish)
+ projects = build_relation(start, finish)
+
+ projects.with_route.find_each(batch_size: BATCH_SIZE) do |project|
+ migrate(project)
+ end
+ end
+
+ # Flag a project to me migrated
+ #
+ # @param [Object] project that will be migrated
+ def migrate(project)
+ Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..."
+
+ project.migrate_to_hashed_storage!
+ rescue => err
+ Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
+ end
+
+ private
+
+ def build_relation(start, finish)
+ relation = Project
+ table = Project.arel_table
+
+ relation = relation.where(table[:id].gteq(start)) if start
+ relation = relation.where(table[:id].lteq(finish)) if finish
+
+ relation
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/hashed_storage/rake_helper.rb b/lib/gitlab/hashed_storage/rake_helper.rb
index 8aba42ccfce..303b05e6a9a 100644
--- a/lib/gitlab/hashed_storage/rake_helper.rb
+++ b/lib/gitlab/hashed_storage/rake_helper.rb
@@ -9,8 +9,20 @@ module Gitlab
ENV.fetch('LIMIT', 500).to_i
end
+ def self.range_from
+ ENV['ID_FROM']
+ end
+
+ def self.range_to
+ ENV['ID_TO']
+ end
+
+ def self.range_single_item?
+ !range_from.nil? && range_from == range_to
+ end
+
def self.project_id_batches(&block)
- Project.with_unmigrated_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches
+ Project.with_unmigrated_storage.in_batches(of: batch_size, start: range_from, finish: range_to) do |relation| # rubocop: disable Cop/InBatches
ids = relation.pluck(:id)
yield ids.min, ids.max
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 4dc38aae61e..e5191f5c7f9 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -30,7 +30,7 @@ module Gitlab
dashboard
deploy.html
explore
- favicon.ico
+ favicon.png
files
groups
health_check
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
index 9277b57f46f..694b01b272c 100644
--- a/lib/gitlab/themes.rb
+++ b/lib/gitlab/themes.rb
@@ -13,15 +13,15 @@ module Gitlab
# All available Themes
THEMES = [
Theme.new(1, 'Indigo', 'ui-indigo'),
- Theme.new(2, 'Light Indigo', 'ui-light-indigo'),
- Theme.new(3, 'Blue', 'ui-blue'),
- Theme.new(4, 'Light Blue', 'ui-light-blue'),
+ Theme.new(6, 'Light Indigo', 'ui-light-indigo'),
+ Theme.new(4, 'Blue', 'ui-blue'),
+ Theme.new(7, 'Light Blue', 'ui-light-blue'),
Theme.new(5, 'Green', 'ui-green'),
- Theme.new(6, 'Light Green', 'ui-light-green'),
- Theme.new(7, 'Red', 'ui-red'),
- Theme.new(8, 'Light Red', 'ui-light-red'),
- Theme.new(9, 'Dark', 'ui-dark'),
- Theme.new(10, 'Light', 'ui-light')
+ Theme.new(8, 'Light Green', 'ui-light-green'),
+ Theme.new(9, 'Red', 'ui-red'),
+ Theme.new(10, 'Light Red', 'ui-light-red'),
+ Theme.new(2, 'Dark', 'ui-dark'),
+ Theme.new(3, 'Light', 'ui-light')
].freeze
# Convenience method to get a space-separated String of all the theme
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index e294f3c4ebc..59a222b086c 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -21,6 +21,7 @@ module Gitlab
uuid: Gitlab::CurrentSettings.uuid,
hostname: Gitlab.config.gitlab.host,
version: Gitlab::VERSION,
+ installation_type: Gitlab::INSTALLATION_TYPE,
active_user_count: User.active.count,
recorded_at: Time.now,
mattermost_enabled: Gitlab.config.mattermost.enabled,
diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake
index 68d6f9d7cb1..f539b1df955 100644
--- a/lib/tasks/gitlab/storage.rake
+++ b/lib/tasks/gitlab/storage.rake
@@ -2,9 +2,26 @@ namespace :gitlab do
namespace :storage do
desc 'GitLab | Storage | Migrate existing projects to Hashed Storage'
task migrate_to_hashed: :environment do
- legacy_projects_count = Project.with_unmigrated_storage.count
+ storage_migrator = Gitlab::HashedStorage::Migrator.new
helper = Gitlab::HashedStorage::RakeHelper
+ if helper.range_single_item?
+ project = Project.with_unmigrated_storage.find_by(id: helper.range_from)
+
+ unless project
+ puts "There are no projects requiring storage migration with ID=#{helper.range_from}"
+
+ next
+ end
+
+ puts "Enqueueing storage migration of #{project.full_path} (ID=#{project.id})..."
+ storage_migrator.migrate(project)
+
+ next
+ end
+
+ legacy_projects_count = Project.with_unmigrated_storage.count
+
if legacy_projects_count == 0
puts 'There are no projects requiring storage migration. Nothing to do!'
@@ -14,7 +31,7 @@ namespace :gitlab do
print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{helper.batch_size}"
helper.project_id_batches do |start, finish|
- StorageMigratorWorker.perform_async(start, finish)
+ storage_migrator.bulk_schedule(start, finish)
print '.'
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f63f2a89aa9..43afb140051 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1086,7 +1086,7 @@ msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
-msgid "ClusterIntegration|Google Cloud Platform project ID"
+msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
msgid "ClusterIntegration|Google Kubernetes Engine"
diff --git a/public/favicon.ico b/public/favicon.ico
deleted file mode 100644
index 3479cbbb46f..00000000000
--- a/public/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/public/favicon.png b/public/favicon.png
new file mode 100644
index 00000000000..845e0ec34a5
--- /dev/null
+++ b/public/favicon.png
Binary files differ
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 1367671e3ca..5bc4ffbb036 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -7,7 +7,7 @@ module QA
class Repository
include Scenario::Actable
- attr_reader :push_error
+ attr_reader :push_output
def self.perform(*args)
Dir.mktmpdir do |dir|
@@ -35,7 +35,7 @@ module QA
end
def clone(opts = '')
- `git clone #{opts} #{@uri.to_s} ./ #{suppress_output}`
+ run_and_redact_credentials("git clone #{opts} #{@uri} ./")
end
def checkout(branch_name)
@@ -71,8 +71,7 @@ module QA
end
def push_changes(branch = 'master')
- # capture3 returns stdout, stderr and status.
- _, @push_error, _ = Open3.capture3("git push #{@uri} #{branch} #{suppress_output}")
+ @push_output, _ = run_and_redact_credentials("git push #{@uri} #{branch}")
end
def commits
@@ -81,12 +80,10 @@ module QA
private
- def suppress_output
- # If we're running as the default user, it's probably a temporary
- # instance and output can be useful for debugging
- return if @username == Runtime::User.default_name
-
- "&> #{File::NULL}"
+ # Since the remote URL contains the credentials, and git occasionally
+ # outputs the URL. Note that stderr is redirected to stdout.
+ def run_and_redact_credentials(command)
+ Open3.capture2("#{command} 2>&1 | sed -E 's#://[^@]+@#://****@#g'")
end
end
end
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
index de849b3eee8..babc0079f3f 100644
--- a/qa/qa/page/project/pipeline/show.rb
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -24,10 +24,10 @@ module QA::Page
end
end
- def has_build?(name, status: :success, wait:)
+ def has_build?(name, status: :success, wait: nil)
within('.pipeline-graph') do
within('.ci-job-component', text: name) do
- has_selector?(".ci-status-icon-#{status}", wait: wait)
+ has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact)
end
end
end
diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb
index 9e438aa3c30..efe7863dc87 100644
--- a/qa/qa/specs/features/repository/protected_branches_spec.rb
+++ b/qa/qa/specs/features/repository/protected_branches_spec.rb
@@ -60,9 +60,9 @@ module QA
push_changes('protected-branch')
end
- expect(repository.push_error)
+ expect(repository.push_output)
.to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/)
- expect(repository.push_error)
+ expect(repository.push_output)
.to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
end
end
diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb
new file mode 100644
index 00000000000..ee1f08da238
--- /dev/null
+++ b/qa/spec/git/repository_spec.rb
@@ -0,0 +1,40 @@
+describe QA::Git::Repository do
+ let(:repository) { described_class.new }
+
+ before do
+ cd_empty_temp_directory
+ set_bad_uri
+ repository.use_default_credentials
+ end
+
+ describe '#clone' do
+ it 'redacts credentials from the URI in output' do
+ output, _ = repository.clone
+
+ expect(output).to include("fatal: unable to access 'http://****@foo/bar.git/'")
+ end
+ end
+
+ describe '#push_changes' do
+ before do
+ `git init` # need a repo to push from
+ end
+
+ it 'redacts credentials from the URI in output' do
+ output, _ = repository.push_changes
+
+ expect(output).to include("error: failed to push some refs to 'http://****@foo/bar.git'")
+ end
+ end
+
+ def cd_empty_temp_directory
+ tmp_dir = 'tmp/git-repository-spec/'
+ FileUtils.rm_r(tmp_dir) if File.exist?(tmp_dir)
+ FileUtils.mkdir_p tmp_dir
+ FileUtils.cd tmp_dir
+ end
+
+ def set_bad_uri
+ repository.uri = 'http://foo/bar.git'
+ end
+end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index a08fcea27a5..06c8a432561 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -265,7 +265,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
- expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico"
+ expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png"
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 6e710c9b20b..22858de0475 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -701,7 +701,7 @@ describe Projects::MergeRequestsController do
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
- expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico"
+ expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png"
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 92886e93077..9618a8417ec 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -253,7 +253,7 @@ describe Projects::PipelinesController do
expect(json_response['text']).to eq status.text
expect(json_response['label']).to eq status.label
expect(json_response['icon']).to eq status.icon
- expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
+ expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png")
end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 376b229ffc9..912aa82526a 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -136,7 +136,7 @@ describe UploadsController do
context 'for PNG files' do
it 'returns Content-Disposition: inline' do
note = create(:note, :with_attachment, project: project)
- get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
expect(response['Content-Disposition']).to start_with('inline;')
end
@@ -145,7 +145,7 @@ describe UploadsController do
context 'for SVG files' do
it 'returns Content-Disposition: attachment' do
note = create(:note, :with_svg_attachment, project: project)
- get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.svg'
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'unsanitized.svg'
expect(response['Content-Disposition']).to start_with('attachment;')
end
@@ -164,7 +164,7 @@ describe UploadsController do
end
it "redirects to the sign in page" do
- get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
+ get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -172,14 +172,14 @@ describe UploadsController do
context "when the user isn't blocked" do
it "responds with status 200" do
- get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
+ get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+ get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png'
response
end
@@ -189,14 +189,14 @@ describe UploadsController do
context "when not signed in" do
it "responds with status 200" do
- get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
+ get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+ get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png'
response
end
@@ -214,14 +214,14 @@ describe UploadsController do
context "when not signed in" do
it "responds with status 200" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
response
end
@@ -234,14 +234,14 @@ describe UploadsController do
end
it "responds with status 200" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
response
end
@@ -256,7 +256,7 @@ describe UploadsController do
context "when not signed in" do
it "redirects to the sign in page" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -279,7 +279,7 @@ describe UploadsController do
end
it "redirects to the sign in page" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -287,14 +287,14 @@ describe UploadsController do
context "when the user isn't blocked" do
it "responds with status 200" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png'
response
end
@@ -304,7 +304,7 @@ describe UploadsController do
context "when the user doesn't have access to the project" do
it "responds with status 404" do
- get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(404)
end
@@ -319,14 +319,14 @@ describe UploadsController do
context "when the group is public" do
context "when not signed in" do
it "responds with status 200" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
response
end
@@ -339,14 +339,14 @@ describe UploadsController do
end
it "responds with status 200" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
response
end
@@ -375,7 +375,7 @@ describe UploadsController do
end
it "redirects to the sign in page" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -383,14 +383,14 @@ describe UploadsController do
context "when the user isn't blocked" do
it "responds with status 200" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png'
response
end
@@ -400,7 +400,7 @@ describe UploadsController do
context "when the user doesn't have access to the project" do
it "responds with status 404" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(404)
end
@@ -420,14 +420,14 @@ describe UploadsController do
context "when not signed in" do
it "responds with status 200" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
response
end
@@ -440,14 +440,14 @@ describe UploadsController do
end
it "responds with status 200" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
response
end
@@ -462,7 +462,7 @@ describe UploadsController do
context "when not signed in" do
it "redirects to the sign in page" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -485,7 +485,7 @@ describe UploadsController do
end
it "redirects to the sign in page" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to redirect_to(new_user_session_path)
end
@@ -493,14 +493,14 @@ describe UploadsController do
context "when the user isn't blocked" do
it "responds with status 200" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(200)
end
it_behaves_like 'content not cached without revalidation' do
subject do
- get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png'
response
end
@@ -510,7 +510,7 @@ describe UploadsController do
context "when the user doesn't have access to the project" do
it "responds with status 404" do
- get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png"
expect(response).to have_gitlab_http_status(404)
end
@@ -560,5 +560,43 @@ describe UploadsController do
end
end
end
+
+ context 'original filename or a version filename must match' do
+ let!(:appearance) { create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') }
+
+ context 'has a valid filename on the original file' do
+ it 'successfully returns the file' do
+ get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'dk.png'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Disposition']).to end_with 'filename="dk.png"'
+ end
+ end
+
+ context 'has an invalid filename on the original file' do
+ it 'returns a 404' do
+ get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'bogus.png'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'has a valid filename on the version file' do
+ it 'successfully returns the file' do
+ get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'favicon_main_dk.png'
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Disposition']).to end_with 'filename="favicon_main_dk.png"'
+ end
+ end
+
+ context 'has an invalid filename on the version file' do
+ it 'returns a 404' do
+ get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'favicon_bogusversion_dk.png'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
end
diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb
index 5ce1988c76f..b77f702f9e1 100644
--- a/spec/factories/project_auto_devops.rb
+++ b/spec/factories/project_auto_devops.rb
@@ -3,5 +3,14 @@ FactoryBot.define do
project
enabled true
domain "example.com"
+ deploy_strategy :continuous
+
+ trait :manual do
+ deploy_strategy :manual
+ end
+
+ trait :disabled do
+ enabled false
+ end
end
end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index d91dcf76191..a5e0ac592b9 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -76,6 +76,26 @@ feature 'Admin Appearance' do
expect(page).not_to have_css(header_logo_selector)
end
+ scenario 'Favicon' do
+ sign_in(create(:admin))
+ visit admin_appearances_path
+
+ attach_file(:appearance_favicon, logo_fixture)
+ click_button 'Save'
+
+ expect(page).to have_css('.appearance-light-logo-preview')
+
+ click_link 'Remove favicon'
+
+ expect(page).not_to have_css('.appearance-light-logo-preview')
+
+ # allowed file types
+ attach_file(:appearance_favicon, Rails.root.join('spec', 'fixtures', 'sanitized.svg'))
+ click_button 'Save'
+
+ expect(page).to have_content 'Favicon You are not allowed to upload "svg" files, allowed types: png, ico'
+ end
+
def expect_custom_sign_in_appearance(appearance)
expect(page).to have_content appearance.title
expect(page).to have_content appearance.description
diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb
index 90cf5a53787..7371a494d36 100644
--- a/spec/features/admin/admin_uses_repository_checks_spec.rb
+++ b/spec/features/admin/admin_uses_repository_checks_spec.rb
@@ -28,7 +28,7 @@ feature 'Admin uses repository checks' do
visit_admin_project_page(project)
page.within('.alert') do
- expect(page.text).to match(/Last repository check \(.* ago\) failed/)
+ expect(page.text).to match(/Last repository check \(just now\) failed/)
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index e414345ac23..f6e0dee28c6 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -150,7 +150,7 @@ describe 'Issue Boards', :js do
click_button 'Add list'
wait_for_requests
- find('.dropdown-menu-close').click
+ find('.js-new-board-list').click
page.within(find('.board:nth-child(2)')) do
accept_confirm { find('.board-delete').click }
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 4625a50b8d9..2cb3ae08b0e 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -143,6 +143,9 @@ describe 'New/edit issue', :js do
click_link label.title
click_link label2.title
end
+
+ find('.js-issuable-form-dropdown.js-label-select').click
+
page.within '.js-label-select' do
expect(page).to have_content label.title
end
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index 7c4fd25bb39..25c408516d1 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -12,7 +12,7 @@ feature 'Merge request > User creates image diff notes', :js do
# Stub helper to return any blob file as image from public app folder.
# This is necessary to run this specs since we don't display repo images in capybara.
allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_url).and_return('/apple-touch-icon.png')
- allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.ico')
+ allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.png')
end
context 'create commit diff notes' do
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index b54addce993..3bd9f5e2298 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -139,7 +139,7 @@ describe 'Merge request > User posts notes', :js do
page.within("#note_#{note.id}") do
is_expected.to have_css('.note_edited_ago')
expect(find('.note_edited_ago').text)
- .to match(/less than a minute ago/)
+ .to match(/just now/)
end
end
end
diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb
index 70e8d436dcb..fafd338e448 100644
--- a/spec/features/projects/labels/subscription_spec.rb
+++ b/spec/features/projects/labels/subscription_spec.rb
@@ -36,7 +36,7 @@ feature 'Labels subscription' do
within "#group_label_#{feature.id}" do
expect(page).not_to have_button 'Unsubscribe'
- click_link_on_dropdown('Group level')
+ click_link_on_dropdown('Subscribe at group level')
expect(page).not_to have_selector('.dropdown-group-label')
expect(page).to have_button 'Unsubscribe'
@@ -45,7 +45,7 @@ feature 'Labels subscription' do
expect(page).to have_selector('.dropdown-group-label')
- click_link_on_dropdown('Project level')
+ click_link_on_dropdown('Subscribe at project level')
expect(page).not_to have_selector('.dropdown-group-label')
expect(page).to have_button 'Unsubscribe'
@@ -68,7 +68,7 @@ feature 'Labels subscription' do
find('.dropdown-group-label').click
page.within('.dropdown-group-label') do
- find('a.js-subscribe-button', text: text).click
+ find('.js-subscribe-button', text: text).click
end
end
end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index ae8b1364ec7..359381c391c 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -102,16 +102,16 @@ feature 'Prioritize labels' do
drag_to(selector: '.label-list-item', from_index: 1, to_index: 2)
page.within('.prioritized-labels') do
- expect(first('li')).to have_content('feature')
- expect(page.all('li').last).to have_content('bug')
+ expect(first('.label-list-item')).to have_content('feature')
+ expect(page.all('.label-list-item').last).to have_content('bug')
end
refresh
wait_for_requests
page.within('.prioritized-labels') do
- expect(first('li')).to have_content('feature')
- expect(page.all('li').last).to have_content('bug')
+ expect(first('.label-list-item')).to have_content('feature')
+ expect(page.all('.label-list-item').last).to have_content('bug')
end
end
diff --git a/spec/features/projects/labels/user_removes_labels_spec.rb b/spec/features/projects/labels/user_removes_labels_spec.rb
index f4fda6de465..efa74015c6e 100644
--- a/spec/features/projects/labels/user_removes_labels_spec.rb
+++ b/spec/features/projects/labels/user_removes_labels_spec.rb
@@ -17,8 +17,9 @@ describe "User removes labels" do
end
it "removes label" do
- page.within(".labels") do
+ page.within(".other-labels") do
page.first(".label-list-item") do
+ first('.js-label-options-dropdown').click
first(".remove-row").click
first(:link, "Delete label").click
end
@@ -36,17 +37,16 @@ describe "User removes labels" do
end
it "removes all labels" do
- page.within(".labels") do
- loop do
- li = page.first(".label-list-item")
- break unless li
+ loop do
+ li = page.first(".label-list-item")
+ break unless li
- li.click_link("Delete")
- click_link("Delete label")
- end
-
- expect(page).to have_content("Generate a default set of labels").and have_content("New label")
+ li.find('.js-label-options-dropdown').click
+ li.click_button("Delete")
+ click_link("Delete label")
end
+
+ expect(page).to have_content("Generate a default set of labels").and have_content("New label")
end
end
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 35776a5f23b..ecc7cf84138 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -344,6 +344,16 @@ describe 'Pipeline', :js do
it 'shows build failure logs' do
expect(page).to have_content('4 examples, 1 failure')
end
+
+ it 'shows the failure reason' do
+ expect(page).to have_content('There is an unknown failure, please try again')
+ end
+
+ it 'shows retry button for failed build' do
+ page.within(find('.build-failures', match: :first)) do
+ expect(page).to have_link('Retry')
+ end
+ end
end
context 'when missing build logs' do
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index 9f285e28535..63e15b365a4 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -29,4 +29,16 @@ describe GroupMembersFinder, '#execute' do
expect(result.to_a).to match_array([member1, member3, member4])
end
+
+ it 'returns members for descendant groups if requested', :nested_groups do
+ member1 = group.add_master(user2)
+ member2 = group.add_master(user1)
+ nested_group.add_master(user2)
+ member3 = nested_group.add_master(user3)
+ member4 = nested_group.add_master(user4)
+
+ result = described_class.new(group).execute(include_descendants: true)
+
+ expect(result.to_a).to match_array([member1, member2, member3, member4])
+ end
end
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index 7bb1f45322e..2fc5299b0f4 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -19,4 +19,16 @@ describe MembersFinder, '#execute' do
expect(result.to_a).to match_array([member1, member2, member3])
end
+
+ it 'includes nested group members if asked', :nested_groups do
+ project = create(:project, namespace: group)
+ nested_group.request_access(user1)
+ member1 = group.add_master(user2)
+ member2 = nested_group.add_master(user3)
+ member3 = project.add_master(user4)
+
+ result = described_class.new(project, user2).execute(include_descendants: true)
+
+ expect(result.to_a).to match_array([member1, member2, member3])
+ end
end
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
index 05922df6b81..b76ec115293 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -37,5 +37,5 @@
"title": { "type": "string" },
"position": { "type": ["integer", "null"] }
},
- "additionalProperties": false
+ "additionalProperties": true
}
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index b77114a8152..cf98eed27f1 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -40,23 +40,6 @@ describe PageLayoutHelper do
end
end
- describe 'favicon' do
- it 'defaults to favicon.ico' do
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
- expect(helper.favicon).to eq 'favicon.ico'
- end
-
- it 'has blue favicon for development' do
- allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
- expect(helper.favicon).to eq 'favicon-blue.ico'
- end
-
- it 'has yellow favicon for canary' do
- stub_env('CANARY', 'true')
- expect(helper.favicon).to eq 'favicon-yellow.ico'
- end
- end
-
describe 'page_image' do
it 'defaults to the GitLab logo' do
expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 9940656fb68..363ebc88afd 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -31,7 +31,7 @@ describe PreferencesHelper do
describe '#user_application_theme' do
context 'with a user' do
it "returns user's theme's css_class" do
- stub_user(theme_id: 10)
+ stub_user(theme_id: 3)
expect(helper.user_application_theme).to eq 'ui-light'
end
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 9b4db774b63..ad263791cd4 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -5,10 +5,10 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import '~/boards/models/assignee';
import eventHub from '~/boards/eventhub';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/list';
import '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card.vue';
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 46fa10e1789..3f5ed4f3d07 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -7,9 +7,9 @@ import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/models/assignee';
import '~/boards/services/board_service';
import '~/boards/stores/boards_store';
import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index abeef272c68..05acf903933 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -5,9 +5,9 @@
import Vue from 'vue';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/models/assignee';
import '~/boards/stores/boards_store';
import '~/boards/components/issue_card_inner';
import { listObj } from './mock_data';
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index d90f9a41231..db68096e3bd 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -3,9 +3,9 @@
import Vue from 'vue';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/models/assignee';
import '~/boards/services/board_service';
import '~/boards/stores/boards_store';
import { mockBoardService } from './mock_data';
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index d5d1139de15..ac8bbb8f2a8 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -6,9 +6,9 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import _ from 'underscore';
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/models/assignee';
import '~/boards/services/board_service';
import '~/boards/stores/boards_store';
import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js
index 797693a21aa..a234c81fadf 100644
--- a/spec/javascripts/boards/modal_store_spec.js
+++ b/spec/javascripts/boards/modal_store_spec.js
@@ -1,9 +1,9 @@
/* global ListIssue */
import '~/vue_shared/models/label';
+import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
-import '~/boards/models/assignee';
import Store from '~/boards/stores/modal_store';
describe('Modal store', () => {
diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js
index a8d09202154..e224ed46d18 100644
--- a/spec/javascripts/datetime_utility_spec.js
+++ b/spec/javascripts/datetime_utility_spec.js
@@ -149,23 +149,22 @@ describe('getSundays', () => {
});
});
-describe('getTimeframeWindow', () => {
- it('returns array of dates representing a timeframe based on provided length and date', () => {
- const date = new Date(2018, 0, 1);
+describe('getTimeframeWindowFrom', () => {
+ it('returns array of date objects upto provided length start with provided startDate', () => {
+ const startDate = new Date(2018, 0, 1);
const mockTimeframe = [
- new Date(2017, 9, 1),
- new Date(2017, 10, 1),
- new Date(2017, 11, 1),
new Date(2018, 0, 1),
new Date(2018, 1, 1),
- new Date(2018, 2, 31),
+ new Date(2018, 2, 1),
+ new Date(2018, 3, 1),
+ new Date(2018, 4, 31),
];
- const timeframe = datetimeUtility.getTimeframeWindow(6, date);
-
- expect(timeframe.length).toBe(6);
+ const timeframe = datetimeUtility.getTimeframeWindowFrom(startDate, 5);
+ expect(timeframe.length).toBe(5);
timeframe.forEach((timeframeItem, index) => {
- expect(timeframeItem.getFullYear() === mockTimeframe[index].getFullYear()).toBeTruthy();
- expect(timeframeItem.getMonth() === mockTimeframe[index].getMonth()).toBeTruthy();
+ console.log(timeframeItem);
+ expect(timeframeItem.getFullYear() === mockTimeframe[index].getFullYear()).toBe(true);
+ expect(timeframeItem.getMonth() === mockTimeframe[index].getMonth()).toBe(true);
expect(timeframeItem.getDate() === mockTimeframe[index].getDate()).toBeTruthy();
});
});
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 25ca8eb6c0b..dd025255bd1 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -20,7 +20,7 @@ export default {
group: 'success',
has_details: true,
details_path: '/root/ci-mock/-/jobs/4757',
- favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ favicon: '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -78,7 +78,7 @@ export default {
group: 'success',
has_details: true,
details_path: '/root/ci-mock/pipelines/140',
- favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ favicon: '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
duration: 6,
finished_at: '2017-06-01T17:32:00.042Z',
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 27f06573432..2d7cc3443cf 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -2,6 +2,7 @@
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import MockAdapter from 'axios-mock-adapter';
+import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data';
describe('common_utils', () => {
describe('parseUrl', () => {
@@ -395,6 +396,7 @@ describe('common_utils', () => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
favicon.setAttribute('href', 'default/favicon');
+ favicon.setAttribute('data-default-href', 'default/favicon');
document.body.appendChild(favicon);
});
@@ -413,7 +415,7 @@ describe('common_utils', () => {
beforeEach(() => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
- favicon.setAttribute('href', 'default/favicon');
+ favicon.setAttribute('data-original-href', 'default/favicon');
document.body.appendChild(favicon);
});
@@ -421,12 +423,43 @@ describe('common_utils', () => {
document.body.removeChild(document.getElementById('favicon'));
});
- it('should reset page favicon to tanuki', () => {
+ it('should reset page favicon to the default icon', () => {
+ const favicon = document.getElementById('favicon');
+ favicon.setAttribute('href', 'new/favicon');
commonUtils.resetFavicon();
expect(document.getElementById('favicon').getAttribute('href')).toEqual('default/favicon');
});
});
+ describe('createOverlayIcon', () => {
+ it('should return the favicon with the overlay', (done) => {
+ commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => {
+ expect(url).toEqual(faviconWithOverlayDataUrl);
+ done();
+ });
+ });
+ });
+
+ describe('setFaviconOverlay', () => {
+ beforeEach(() => {
+ const favicon = document.createElement('link');
+ favicon.setAttribute('id', 'favicon');
+ favicon.setAttribute('data-original-href', faviconDataUrl);
+ document.body.appendChild(favicon);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(document.getElementById('favicon'));
+ });
+
+ it('should set page favicon to provided favicon overlay', (done) => {
+ commonUtils.setFaviconOverlay(overlayDataUrl).then(() => {
+ expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl);
+ done();
+ });
+ });
+ });
+
describe('setCiStatusFavicon', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`;
let mock;
@@ -434,6 +467,8 @@ describe('common_utils', () => {
beforeEach(() => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
+ favicon.setAttribute('href', 'null');
+ favicon.setAttribute('data-original-href', faviconDataUrl);
document.body.appendChild(favicon);
mock = new MockAdapter(axios);
});
@@ -449,7 +484,7 @@ describe('common_utils', () => {
commonUtils.setCiStatusFavicon(BUILD_URL)
.then(() => {
const favicon = document.getElementById('favicon');
- expect(favicon.getAttribute('href')).toEqual('null');
+ expect(favicon.getAttribute('href')).toEqual(faviconDataUrl);
done();
})
// Error is already caught in catch() block of setCiStatusFavicon,
@@ -458,16 +493,14 @@ describe('common_utils', () => {
});
it('should set page favicon to CI status favicon based on provided status', (done) => {
- const FAVICON_PATH = '//icon_status_success';
-
mock.onGet(BUILD_URL).reply(200, {
- favicon: FAVICON_PATH,
+ favicon: overlayDataUrl,
});
commonUtils.setCiStatusFavicon(BUILD_URL)
.then(() => {
const favicon = document.getElementById('favicon');
- expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH);
+ expect(favicon.getAttribute('href')).toEqual(faviconWithOverlayDataUrl);
done();
})
.catch(done.fail);
diff --git a/spec/javascripts/lib/utils/mock_data.js b/spec/javascripts/lib/utils/mock_data.js
new file mode 100644
index 00000000000..fd0d62b751f
--- /dev/null
+++ b/spec/javascripts/lib/utils/mock_data.js
@@ -0,0 +1,5 @@
+export const faviconDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACcFBMVEX////iQyniQyniQyniQyniQyniQyniQyniQynhRiriQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniRCniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQynhQiniQiniQiniQinhQinpUSjqUSjqTyjqTyjqTyjlSCniRCniQynjRCjqTyjsZSjrWyj8oib9kSb8pyb9pib8oyb8fyb3ZSb4Zib8fCb8oyb8oyb8oyb8pCb8cSbiQyn7bCb8cib8oyb8oSb8bSbtVSjpTij8nyb8oyb8oyb8lCb2Yyf3ZCf8mCb8oyb8oyb8oyb8iib8bSbiRCn8gyb8oyb8eCbpTinrUSj8oyb8oyb8oyb8pSb8bib4Zif0YCf8byb8oyb8oyb8oyb7oib8oyb8nCbjRSn9bib8ayb8nib8oyb8oyb8oyb8kSbpTyjpTyj8jib8oyb8oyb8oyb8fib0Xyf2ZSb8gCb8oyb6pSb8oyb8dib+cCbgQCnjRSn8cCb8oib8oyb8oyb8oybqUCjnSyn8bCb8oyb8oyb8oyb8myb2YyfyXyf8oyb8oyb8hibhQSn+bib8iSb8oyb8qCb+fSbmSSnqTyj8oib9pCb1YifxXyf7pSb8oCb8pCb+mCb0fCf8pSb7hSXvcSjiQyniQinqTyj9kCb9bib9byb+cCbqUSjiRCnsVCj+cSb8pib8bCb8bSbgQCn7bCb8bibjRSn8oyb8ayb8oib8aib8pCbjRCn8pybhQinhQSn8pSb7ayb7aSb6aib8eib///8IbM+7AAAAr3RSTlMBA3NtX2vT698HGQcRLwWLiXnv++3V+eEd/R8HE2V/Y5HjyefdFw99YWfJ+/3nwQP78/HvX1VTQ/kdA2HzbQXj9fX79/3DGf379/33T/v99/f7ba33+/f1+9/18/v59V339flzF/H9+fX3/fMhBwOh9/v5/fmvBV/z+fP3Awnp9/f38+UFgff7+/37+4c77/f7/flFz/f59dFr7/v98Wnr+/f3I5/197EDBU1ZAwUD8/kLUwAAAAFiS0dEAIgFHUgAAAAHdElNRQfhBQoLHiBV6/1lAAACHUlEQVQ4y41TZXsTQRCe4FAIUigN7m7FXY+iLRQKBG2x4g7BjhZ3Le7uMoEkFJprwyQk0CC/iZnNhUZaHt4vt6/szO7cHcD/wFKjZrJWq3YMq1M3eVc9rFzXR2yQkuA3RGxkjZLGiEk9miA2tURJs1RsnhhokYYtzaU13WZDbBVnW1sjo43J2vI6tZ0lLtFeAh1M0lECneI7dGYtrUtk3RUVIKaEJR25qw27yT0s3W0qEHuPlB4RradivXo7GX36xnbo51SQ+fWHARmCgYMGDxkaxbD3SssYPmIkwKgPLrfA87EETTg/fVaSa/SYsQDjSsd7DcGEsr+BieVKmaRNBsjUtClTfUI900y/5Mt05c8oJQKYSURZ2UqYFa0w283M588JEM2BuRwI5EqT8nmmXzZf4l8XsGNfCIv4QcHFklhiBpaqAsuC4tghj+ySyOdjeJYrP7RCCuR/E5tWAqxaLcmCNSyujdxjHZdbn8UHoA0bN/GoNm8hjQJb/ZzYpo6w3TB27JRduxxqrA7YzbWCezixN8RD2Oc2/Ptlfx7o5uT1A4XMiwzj4HfEikNe7+Ew0ZGjeuW70eEYaeHjxomTiKd++E4XnKGz8d+HDufOB3Ky3RcwdNF1qZiKLyf/B44r2tWf15wV143cwI2qfi8dbtKtX6Hbd+6G74EDqkTm/QcPH/0ufFyNLXjy9NnzF9Xb8BJevYY38C+8fZcg/AF3QTYemVkCwwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNy0wNS0xMFQxMTozMDozMiswMjowMMzup8UAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTctMDUtMTBUMTE6MzA6MzIrMDI6MDC9sx95AAAAAElFTkSuQmCC';
+
+export const overlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA85JREFUWAntVllIVGEUPv/9b46O41KplYN7PeRkti8TjQlhCUGh3MmeQugpIsGKAi2soIcIooiohxYKK2daqDAlIpIiWwxtQaJcaHE0d5tMrbn37z9XRqfR0TvVW56Hudf//uec72zfEWBCJjIwkYGJDPzvGSD/KgExN3Oi2Q+2DJgSDYQEMwItVGH1iZGmJw/Si1y+/PwVAMYYib22MYc/8hVQFgKDEfYoId0KYzagAQebsos/ewMZoeB9wdffcTYpQSaCTWHKoqSQaDk7zkIt0+aCUR8BelEHrf3dUNv9AcqbnsHtT5UKB/hTASh0SLYjnjb/CIDRJi0XiFAaJOpCD8zLpdb4NB66b1OfelthX815dtdRRfiti2aAXLvVLiMQ6olGyztGDkSo4JGGXk8/QFdGpYzpHG2GBQTDhtgVhPEaVbbVpvI6GJz22rv4TcAfrYI1x7Rj5MWWAppomKFVVb2302SFzUkZHAbkG+0b1+Gh77yNYjrmqnWTrLBLRxdvBWv8qlFujH/kYjJYyvLkj71t78zAUvzMAMnHhpN4zf9UREJhd8omyssxu1IgazQDwDnHUcNuH6vhPIE1fmuBzHt74Hn7W89jWGtcAjoaIDOFrdcMYJBkgOCoaRF0Lj0oglddDbCj6tRvKjphEpgjkzEQs2YAKsNxMzjn3nKurhzK+Ly7xe28ua8TwgMMcHJZnvvT0BPtEEKM4tDJ+C8GvIIk4ylINIXVZ0EUKJxYuh3mhCeokbudl6TtVc88dfBdLwbyaWB6zQCYQJpBYSrDGQxBQ/ZWRM2B+VNmQnVnHWx7elyNuL2/R336co7KyJR8CL9oLgEuFlREevWUkEl6uGwpVEG4FBm0OEf9N10NMgPlvWYAuNVwsWDKvcUNYsHUWTCZ13ysyFEXe6TO6aC8CUr9IiK+A05TQrc8yjwmxARHeeMAPlfQJw+AQRwu0YhL/GDXi9NwufG+S8dYkuYMqIb4SsWthotlNMOUCOM6r+G9cqXxPmd1dqrBav/o1zJy2l5/NUjJA/VORwYuFnOUaTQcPs9wMqwV++Xv8oADxKAcZ8nLPr8AoGW+xR6HSqYk3GodAz2QNj0V+Gr26dT9ASNH5239Pf0gktVNWZca8ZvfAFBprWS6hSu1pqt++Y0PD+WIwDAhIWQGtzvSHDbcodfFUFB9hg1Gjs5LXqIdFL+acFBl+FddqYwdxsWC3I70OvgfUaA65zhq2O2c8VxYcyIGFTVlXegYtvCXANCQZJMobjVcLMjtSK/IcEgyOOe8Ve5w7ryKDefp2P3+C/5ohv8HZmVLAAAAAElFTkSuQmCC';
+
+export const faviconWithOverlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGt0lEQVRYR8WWf3DT5R3H35/vN0lDiCztSuiPAEnTFhSOUamIzFGokwMH55g0E845yjbP6+4qIoiHY6JjnHLeRI6h9jgQpQcD3ImH2u00eHBwjGthKLI1TSm26S8oKYVS0vT7/X52z7ckTUhqC/yx55/kks/zeb+ez6/nIWYm/B8XJQB4SGq6lL+CJA47vvRtvWs2D0nNl/Kf0qBZxx6u23arv0QAAIHivK8BynB4ffa7BgDQXJx/ngGnw+uThgTwP5ZnMocoJAxVxZDVZ+0L5n5WF75TkMafjLdJxpSg2E+gqW1X7zk3rbpaifhLiEBgTv4mEFbpBoTyu01DoDj/dQAv9rvjtdnp/k3Yx9rgAMV5QYCsAAwAgg6vL/1OTy/2BYrzzwLIBWACuNHhrXPG+otGoKaw0JA58kqGJtOFfgPS8yWT4sz88nzj7UIIfz+wd0mRdEZPLMnp2V/8R0+JrhLbBYFHJvwWzBUxYgqYNzhG+zfEhm24MIE5ectBtP0W+y0Or29FcoDifHFSRxwAcMrh9c0YrmisXaA4r0V0U8xvopgDDq9PpCQ+Ag0/zbEbNUNbMiG9fTwkDTsKHpJa2t1Zmiw1AqLg+tMZ+R6WVVtnZ2qP6Ib+FIjh05G3lsDrB4xjUIbRDeM+WZLJYZ4B1rKMzKPm/fdyzs9qg6WT225IMnPcuYjxbPZhn57qaA00zc4/QYT7b1b/wAZmDYSLjsN1WcmiM+6jXz7JTCs1aNPASBjrtrCGOXVBLK9ph72772bc0REZcsQlkEVoOxblhaFBH0Bxi6GBWFNC8gpV0XqYSe/hI85R9o1zxr/QaZbdbmuW9oRzljRrzBRkW9JhMaTgYugKzl35DlXNJ/Fp43FImoZnz7T0ln7bLihM0g85N627vkWPgLrbvYyCvAP1+rRIWETA5QsyQlcJYOCbMRasWpALtljwSsFyeJxFYsoNWqdN1y/ildM78Y/WGjxx8TL+ol3oluy8VupKe7cfoNLdCJkdqEUPOmBJ5ksJoae91mBps5lQ6pkIm20MPiz6A3KsmcNukDe/3Ye3zh3A77Q2XqcGjslLz88i/nB8pkpSoL8nAFSTBpUN4qSxS5KB5jOGUOniCebmzFQcevSN2xKP+Fp7ajt21f8TOxU/5i45JZFS6XwcTB9HxZgUnGTRNgk31x5jet+aGU7jWw+UweOcPeyTxxoqrGL25+UwdjehSvnmOVIqcz4C8y8GAABcQwjnYI5NheikhQWT+EZmDh2ev/l7cz4U2cGmYyg78TYqVH87Kbtd1wFY4hsVQAt14zu2RiDaTUZMf/BHWD35STx37wDv94k1dLeh7MRmvDZ1GR5Inxg17dX6MPnjZfh5X6tGSqXrV2B8ACIx98UNGOlV4CxCuA6zqIeq9FQ8c68bhx7ZiIK06CQdVF+Il3y1Hq03gnDfk4Uj8zbH2T51dCPOtlW39Q+iPTl2VSMfwKPiKw8aTuhgpl1Zdqxzj8PphRWwm21xZjv9VcgYkYb52dP132PFbSYr/la0DpNtrrg9a2oqsKfB2zlwG+4nSe1z7QDjaQBi2Eh6J4QRwimYt43LwOsuB2oX7YLVMCLqTAya3xx/EwZJxtYHy3WhyMkHExebXz3zAbbXfdo7AFBRaMAz1Ypa6XoaoPejKRGteZm6D3SlWVdOcOHo/Lfj2u9aXw+WHNmA00G/DiFEO0Jd+meyk0fIf/+vLfik6Xhj4qN0v7i5HCY1bBQPk+ij9GSzNbzYNdH03kMrscARfzvHQgiBocSFTVHVCrW+u+WrpK9iCIgS1rRK93oG/1GkRJVIup8KMNs1Sw/1rUtALD36ZzRca8XeJDmPtRc18vDn5SCJViYHENY3IZTK3JkE7RAYtpdkp3bAaJeOzN+CsSMTX+wqa7ih9sbVSLI2WV3znihAJYXZPThA7M6KQoM2MniyhUxTioxTpKLMadjx8Jqh5k3S//8d9GOh92XWmP/aXLKvfHgA0ZTklL0jj9m6UR6L5+9bjFWTPLcFIWbCY1+8pHb0drWybJ4aWLQrODyAWJndzoyylNyGg0hL+bV7Ll4rKIWB5CFBxMlLj21SL4W6QjDQjwOL9n4tNt0+AADPfo+UqgXPHJLSJrkso7F6ylLMy56OFMmYACIKblvtQext8Iqp0swyLYiI3zEAbs6Ml3cXv/p3Y+ryq5KcnSKb1Jmj75P7X0Rm/UV0tvO86r/WIhORwszvkmHEehH2WMo7ikDUQUWhoaIG+NNc96Os8eMEmklE2Qy2ANTO0OrA+CwFOFBfsq8pWZ7+B25aDBxvPp+QAAAAAElFTkSuQmCC';
diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/javascripts/lib/utils/url_utility_spec.js
new file mode 100644
index 00000000000..c7f4092911c
--- /dev/null
+++ b/spec/javascripts/lib/utils/url_utility_spec.js
@@ -0,0 +1,29 @@
+import { webIDEUrl } from '~/lib/utils/url_utility';
+
+describe('URL utility', () => {
+ describe('webIDEUrl', () => {
+ afterEach(() => {
+ gon.relative_url_root = '';
+ });
+
+ describe('without relative_url_root', () => {
+ it('returns IDE path with route', () => {
+ expect(webIDEUrl('/gitlab-org/gitlab-ce/merge_requests/1')).toBe(
+ '/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1',
+ );
+ });
+ });
+
+ describe('with relative_url_root', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/gitlab';
+ });
+
+ it('returns IDE path with route', () => {
+ expect(webIDEUrl('/gitlab/gitlab-org/gitlab-ce/merge_requests/1')).toBe(
+ '/gitlab/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1',
+ );
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js
index 70eba98e939..9e25a4b3fed 100644
--- a/spec/javascripts/pipelines/graph/mock_data.js
+++ b/spec/javascripts/pipelines/graph/mock_data.js
@@ -20,7 +20,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/pipelines/123',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
duration: 9,
finished_at: '2017-04-19T14:30:27.542Z',
@@ -40,7 +40,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4153',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -65,7 +65,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4153',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -85,7 +85,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/pipelines/123#test',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
path: '/root/ci-mock/pipelines/123#test',
dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test',
@@ -105,7 +105,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4166',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -130,7 +130,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4166',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -152,7 +152,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4159',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -177,7 +177,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/builds/4159',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
action: {
icon: 'retry',
title: 'Retry',
@@ -197,7 +197,7 @@ export default {
has_details: true,
details_path: '/root/ci-mock/pipelines/123#deploy',
favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
},
path: '/root/ci-mock/pipelines/123#deploy',
dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy',
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 9b9c9656979..3d36e46d863 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -12,6 +12,7 @@ describe('MRWidgetHeader', () => {
afterEach(() => {
vm.$destroy();
+ gon.relative_url_root = '';
});
describe('computed', () => {
@@ -145,7 +146,16 @@ describe('MRWidgetHeader', () => {
const button = vm.$el.querySelector('.js-web-ide');
expect(button.textContent.trim()).toEqual('Web IDE');
- expect(button.getAttribute('href')).toEqual('undefined/-/ide/projectabc');
+ expect(button.getAttribute('href')).toEqual('/-/ide/projectabc');
+ });
+
+ it('renders web ide button with relative URL', () => {
+ gon.relative_url_root = '/gitlab';
+
+ const button = vm.$el.querySelector('.js-web-ide');
+
+ expect(button.textContent.trim()).toEqual('Web IDE');
+ expect(button.getAttribute('href')).toEqual('/-/ide/projectabc');
});
it('renders download dropdown with links', () => {
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 30918428da2..6342ea00436 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -5,6 +5,7 @@ import notify from '~/lib/utils/notify';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './mock_data';
+import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from '../lib/utils/mock_data';
const returnPromise = data => new Promise((resolve) => {
resolve({
@@ -273,6 +274,7 @@ describe('mrWidgetOptions', () => {
beforeEach(() => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
+ favicon.setAttribute('data-original-href', faviconDataUrl);
document.body.appendChild(favicon);
faviconElement = document.getElementById('favicon');
@@ -282,10 +284,13 @@ describe('mrWidgetOptions', () => {
document.body.removeChild(document.getElementById('favicon'));
});
- it('should call setFavicon method', () => {
- vm.setFaviconHelper();
-
- expect(faviconElement.getAttribute('href')).toEqual(vm.mr.ciStatusFaviconPath);
+ it('should call setFavicon method', (done) => {
+ vm.mr.ciStatusFaviconPath = overlayDataUrl;
+ vm.setFaviconHelper().then(() => {
+ expect(faviconElement.getAttribute('href')).toEqual(faviconWithOverlayDataUrl);
+ done();
+ })
+ .catch(done.fail);
});
it('should not call setFavicon when there is no ciStatusFaviconPath', () => {
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 19028495f52..55490f37ac7 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -5,6 +5,13 @@ describe Gitlab::CurrentSettings do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
+ shared_context 'with settings in cache' do
+ before do
+ create(:application_setting)
+ described_class.current_application_settings # warm the cache
+ end
+ end
+
describe '#current_application_settings', :use_clean_rails_memory_store_caching do
it 'allows keys to be called directly' do
db_settings = create(:application_setting,
@@ -31,16 +38,29 @@ describe Gitlab::CurrentSettings do
end
context 'with DB unavailable' do
- before do
- # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
- # during the initialization phase of the test suite, so instead let's mock the internals of it
- allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
+ context 'and settings in cache' do
+ include_context 'with settings in cache'
+
+ it 'fetches the settings from cache without issuing any query' do
+ expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
+ end
end
- it 'returns an in-memory ApplicationSetting object' do
- expect(ApplicationSetting).not_to receive(:current)
+ context 'and no settings in cache' do
+ before do
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
+ expect(ApplicationSetting).not_to receive(:current)
+ end
- expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
+
+ it 'does not issue any query' do
+ expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
+ end
end
end
@@ -52,73 +72,86 @@ describe Gitlab::CurrentSettings do
ar_wrapped_defaults.slice(*::ApplicationSetting.defaults.keys)
end
- before do
- # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
- # during the initialization phase of the test suite, so instead let's mock the internals of it
- allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
- allow(ActiveRecord::Base.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true)
- end
+ context 'and settings in cache' do
+ include_context 'with settings in cache'
- it 'creates default ApplicationSettings if none are present' do
- settings = described_class.current_application_settings
-
- expect(settings).to be_a(ApplicationSetting)
- expect(settings).to be_persisted
- expect(settings).to have_attributes(settings_from_defaults)
+ it 'fetches the settings from cache' do
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ expect(ActiveRecord::Base.connection).not_to receive(:active?)
+ expect(ActiveRecord::Base.connection).not_to receive(:cached_table_exists?)
+ expect(ActiveRecord::Migrator).not_to receive(:needs_migration?)
+ expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
+ end
end
- context 'with migrations pending' do
+ context 'and no settings in cache' do
before do
- expect(ActiveRecord::Migrator).to receive(:needs_migration?).and_return(true)
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
+ allow(ActiveRecord::Base.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true)
end
- it 'returns an in-memory ApplicationSetting object' do
+ it 'creates default ApplicationSettings if none are present' do
settings = described_class.current_application_settings
- expect(settings).to be_a(Gitlab::FakeApplicationSettings)
- expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
- expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled)
+ expect(settings).to be_a(ApplicationSetting)
+ expect(settings).to be_persisted
+ expect(settings).to have_attributes(settings_from_defaults)
end
- it 'uses the existing database settings and falls back to defaults' do
- db_settings = create(:application_setting,
- home_page_url: 'http://mydomain.com',
- signup_enabled: false)
- settings = described_class.current_application_settings
- app_defaults = ApplicationSetting.last
-
- expect(settings).to be_a(Gitlab::FakeApplicationSettings)
- expect(settings.home_page_url).to eq(db_settings.home_page_url)
- expect(settings.signup_enabled?).to be_falsey
- expect(settings.signup_enabled).to be_falsey
-
- # Check that unspecified values use the defaults
- settings.reject! { |key, _| [:home_page_url, :signup_enabled].include? key }
- settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) }
+ context 'with migrations pending' do
+ before do
+ expect(ActiveRecord::Migrator).to receive(:needs_migration?).and_return(true)
+ end
+
+ it 'returns an in-memory ApplicationSetting object' do
+ settings = described_class.current_application_settings
+
+ expect(settings).to be_a(Gitlab::FakeApplicationSettings)
+ expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
+ expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled)
+ end
+
+ it 'uses the existing database settings and falls back to defaults' do
+ db_settings = create(:application_setting,
+ home_page_url: 'http://mydomain.com',
+ signup_enabled: false)
+ settings = described_class.current_application_settings
+ app_defaults = ApplicationSetting.last
+
+ expect(settings).to be_a(Gitlab::FakeApplicationSettings)
+ expect(settings.home_page_url).to eq(db_settings.home_page_url)
+ expect(settings.signup_enabled?).to be_falsey
+ expect(settings.signup_enabled).to be_falsey
+
+ # Check that unspecified values use the defaults
+ settings.reject! { |key, _| [:home_page_url, :signup_enabled].include? key }
+ settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) }
+ end
end
- end
- context 'when ApplicationSettings.current is present' do
- it 'returns the existing application settings' do
- expect(ApplicationSetting).to receive(:current).and_return(:current_settings)
+ context 'when ApplicationSettings.current is present' do
+ it 'returns the existing application settings' do
+ expect(ApplicationSetting).to receive(:current).and_return(:current_settings)
- expect(described_class.current_application_settings).to eq(:current_settings)
+ expect(described_class.current_application_settings).to eq(:current_settings)
+ end
end
- end
- context 'when the application_settings table does not exists' do
- it 'returns an in-memory ApplicationSetting object' do
- expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::StatementInvalid)
+ context 'when the application_settings table does not exists' do
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::StatementInvalid)
- expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
end
- end
- context 'when the application_settings table is not fully migrated' do
- it 'returns an in-memory ApplicationSetting object' do
- expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::UnknownAttributeError)
+ context 'when the application_settings table is not fully migrated' do
+ it 'returns an in-memory ApplicationSetting object' do
+ expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::UnknownAttributeError)
- expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
+ end
end
end
end
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
new file mode 100644
index 00000000000..fdc5c0180e4
--- /dev/null
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -0,0 +1,52 @@
+require 'rails_helper'
+
+RSpec.describe Gitlab::Favicon, :request_store do
+ describe '.main' do
+ it 'defaults to favicon.png' do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
+ expect(described_class.main).to match_asset_path '/assets/favicon.png'
+ end
+
+ it 'has blue favicon for development' do
+ allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
+ expect(described_class.main).to match_asset_path '/assets/favicon-blue.png'
+ end
+
+ it 'has yellow favicon for canary' do
+ stub_env('CANARY', 'true')
+ expect(described_class.main).to match_asset_path 'favicon-yellow.png'
+ end
+
+ it 'uses the custom favicon if a favicon appearance is present' do
+ create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'))
+ expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/favicon_main_dk.png}
+ end
+ end
+
+ describe '.status_overlay' do
+ subject { described_class.status_overlay('favicon_status_created') }
+
+ it 'returns the overlay for the status' do
+ expect(subject).to match_asset_path '/assets/ci_favicons/favicon_status_created.png'
+ end
+ end
+
+ describe '.available_status_names' do
+ subject { described_class.available_status_names }
+
+ it 'returns the available status names' do
+ expect(subject).to eq %w(
+ favicon_status_canceled
+ favicon_status_created
+ favicon_status_failed
+ favicon_status_manual
+ favicon_status_not_found
+ favicon_status_pending
+ favicon_status_running
+ favicon_status_skipped
+ favicon_status_success
+ favicon_status_warning
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
new file mode 100644
index 00000000000..813ae43b4d3
--- /dev/null
+++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe Gitlab::HashedStorage::Migrator do
+ describe '#bulk_schedule' do
+ it 'schedules job to StorageMigratorWorker' do
+ Sidekiq::Testing.fake! do
+ expect { subject.bulk_schedule(1, 5) }.to change(StorageMigratorWorker.jobs, :size).by(1)
+ end
+ end
+ end
+
+ describe '#bulk_migrate' do
+ let(:projects) { create_list(:project, 2, :legacy_storage) }
+ let(:ids) { projects.map(&:id) }
+
+ it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do
+ Sidekiq::Testing.fake! do
+ expect { subject.bulk_migrate(ids.min, ids.max) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(2)
+ end
+ end
+
+ it 'sets projects as read only' do
+ allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice
+ subject.bulk_migrate(ids.min, ids.max)
+
+ projects.each do |project|
+ expect(project.reload.repository_read_only?).to be_truthy
+ end
+ end
+
+ it 'rescues and log exceptions' do
+ allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError)
+ expect { subject.bulk_migrate(ids.min, ids.max) }.not_to raise_error
+ end
+
+ it 'delegates each project in specified range to #migrate' do
+ projects.each do |project|
+ expect(subject).to receive(:migrate).with(project)
+ end
+
+ subject.bulk_migrate(ids.min, ids.max)
+ end
+ end
+
+ describe '#migrate' do
+ let(:project) { create(:project, :legacy_storage, :empty_repo) }
+
+ it 'enqueues job to ProjectMigrateHashedStorageWorker' do
+ Sidekiq::Testing.fake! do
+ expect { subject.migrate(project) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1)
+ end
+ end
+
+ it 'rescues and log exceptions' do
+ allow(project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError)
+
+ expect { subject.migrate(project) }.not_to raise_error
+ end
+
+ it 'sets project as read only' do
+ allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async)
+ subject.migrate(project)
+
+ expect(project.reload.repository_read_only?).to be_truthy
+ end
+
+ it 'migrate project' do
+ Sidekiq::Testing.inline! do
+ subject.migrate(project)
+ end
+
+ expect(project.reload.hashed_storage?(:attachments)).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 5b289ceb3b2..4354dca25ea 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -540,6 +540,7 @@ ProjectAutoDevops:
- id
- enabled
- domain
+- deploy_strategy
- project_id
- created_at
- updated_at
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
index af2f4568017..a8213988f70 100644
--- a/spec/lib/gitlab/themes_spec.rb
+++ b/spec/lib/gitlab/themes_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Themes, lib: true do
css = described_class.body_classes
expect(css).to include('ui-indigo')
- expect(css).to include('ui-dark ')
+ expect(css).to include('ui-dark')
expect(css).to include('ui-blue')
end
end
@@ -14,7 +14,7 @@ describe Gitlab::Themes, lib: true do
describe '.by_id' do
it 'returns a Theme by its ID' do
expect(described_class.by_id(1).name).to eq 'Indigo'
- expect(described_class.by_id(10).name).to eq 'Light'
+ expect(described_class.by_id(3).name).to eq 'Light'
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index a716e6f5434..22d921716aa 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -32,6 +32,7 @@ describe Gitlab::UsageData do
mattermost_enabled
edition
version
+ installation_type
uuid
hostname
signup
@@ -156,6 +157,7 @@ describe Gitlab::UsageData do
it "gathers license data" do
expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid)
expect(subject[:version]).to eq(Gitlab::VERSION)
+ expect(subject[:installation_type]).to eq(Gitlab::INSTALLATION_TYPE)
expect(subject[:active_user_count]).to eq(User.active.count)
expect(subject[:recorded_at]).to be_a(Time)
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index f83b52e8975..9fe1186a8c9 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -67,6 +67,30 @@ describe Group do
end
end
+ describe '#notification_settings', :nested_groups do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:sub_group) { create(:group, parent_id: group.id) }
+
+ before do
+ group.add_developer(user)
+ sub_group.add_developer(user)
+ end
+
+ it 'also gets notification settings from parent groups' do
+ expect(sub_group.notification_settings.size).to eq(2)
+ expect(sub_group.notification_settings).to include(group.notification_settings.first)
+ end
+
+ context 'when sub group is deleted' do
+ it 'does not delete parent notification settings' do
+ expect do
+ sub_group.destroy
+ end.to change { NotificationSetting.count }.by(-1)
+ end
+ end
+ end
+
describe '#visibility_level_allowed_by_parent' do
let(:parent) { create(:group, :internal) }
let(:sub_group) { build(:group, parent_id: parent.id) }
@@ -240,7 +264,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(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff"])
+ 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, ico"])
end
end
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index eda0e1da835..13fe47799ed 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -13,4 +13,48 @@ describe NotificationRecipient do
expect(recipient.has_access?).to be_falsy
end
+
+ context '#notification_setting' do
+ context 'for child groups', :nested_groups do
+ let!(:moved_group) { create(:group) }
+ let(:group) { create(:group) }
+ let(:sub_group_1) { create(:group, parent: group) }
+ let(:sub_group_2) { create(:group, parent: sub_group_1) }
+ let(:project) { create(:project, namespace: moved_group) }
+
+ before do
+ sub_group_2.add_owner(user)
+ moved_group.add_owner(user)
+ Groups::TransferService.new(moved_group, user).execute(sub_group_2)
+
+ moved_group.reload
+ end
+
+ context 'when notification setting is global' do
+ before do
+ user.notification_settings_for(group).global!
+ user.notification_settings_for(sub_group_1).mention!
+ user.notification_settings_for(sub_group_2).global!
+ user.notification_settings_for(moved_group).global!
+ end
+
+ it 'considers notification setting from the first parent without global setting' do
+ expect(subject.notification_setting.source).to eq(sub_group_1)
+ end
+ end
+
+ context 'when notification setting is not global' do
+ before do
+ user.notification_settings_for(group).global!
+ user.notification_settings_for(sub_group_1).mention!
+ user.notification_settings_for(sub_group_2).watch!
+ user.notification_settings_for(moved_group).disabled!
+ end
+
+ it 'considers notification setting from lowest group member in hierarchy' do
+ expect(subject.notification_setting.source).to eq(moved_group)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
index 7545c0797e9..749b2094787 100644
--- a/spec/models/project_auto_devops_spec.rb
+++ b/spec/models/project_auto_devops_spec.rb
@@ -5,6 +5,8 @@ describe ProjectAutoDevops do
it { is_expected.to belong_to(:project) }
+ it { is_expected.to define_enum_for(:deploy_strategy) }
+
it { is_expected.to respond_to(:created_at) }
it { is_expected.to respond_to(:updated_at) }
@@ -67,8 +69,127 @@ describe ProjectAutoDevops do
end
end
+ context 'when deploy_strategy is manual' do
+ let(:domain) { 'example.com' }
+
+ before do
+ auto_devops.deploy_strategy = 'manual'
+ end
+
+ it do
+ expect(auto_devops.predefined_variables.map { |var| var[:key] })
+ .to include("STAGING_ENABLED", "INCREMENTAL_ROLLOUT_ENABLED")
+ end
+ end
+
+ context 'when deploy_strategy is continuous' do
+ let(:domain) { 'example.com' }
+
+ before do
+ auto_devops.deploy_strategy = 'continuous'
+ end
+
+ it do
+ expect(auto_devops.predefined_variables.map { |var| var[:key] })
+ .not_to include("STAGING_ENABLED", "INCREMENTAL_ROLLOUT_ENABLED")
+ end
+ end
+
def domain_variable
{ key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }
end
end
+
+ describe '#set_gitlab_deploy_token' do
+ let(:auto_devops) { build(:project_auto_devops, project: project) }
+
+ context 'when the project is public' do
+ let(:project) { create(:project, :repository, :public) }
+
+ it 'should not create a gitlab deploy token' do
+ expect do
+ auto_devops.save
+ end.not_to change { DeployToken.count }
+ end
+ end
+
+ context 'when the project is internal' do
+ let(:project) { create(:project, :repository, :internal) }
+
+ it 'should create a gitlab deploy token' do
+ expect do
+ auto_devops.save
+ end.to change { DeployToken.count }.by(1)
+ end
+ end
+
+ context 'when the project is private' do
+ let(:project) { create(:project, :repository, :private) }
+
+ it 'should create a gitlab deploy token' do
+ expect do
+ auto_devops.save
+ end.to change { DeployToken.count }.by(1)
+ end
+ end
+
+ context 'when autodevops is enabled at project level' do
+ let(:project) { create(:project, :repository, :internal) }
+ let(:auto_devops) { build(:project_auto_devops, project: project) }
+
+ it 'should create a deploy token' do
+ expect do
+ auto_devops.save
+ end.to change { DeployToken.count }.by(1)
+ end
+ end
+
+ context 'when autodevops is enabled at instancel level' do
+ let(:project) { create(:project, :repository, :internal) }
+ let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) }
+
+ it 'should create a deploy token' do
+ allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true)
+
+ expect do
+ auto_devops.save
+ end.to change { DeployToken.count }.by(1)
+ end
+ end
+
+ context 'when autodevops is disabled' do
+ let(:project) { create(:project, :repository, :internal) }
+ let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) }
+
+ it 'should not create a deploy token' do
+ expect do
+ auto_devops.save
+ end.not_to change { DeployToken.count }
+ end
+ end
+
+ context 'when the project already has an active gitlab-deploy-token' do
+ let(:project) { create(:project, :repository, :internal) }
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) }
+ let(:auto_devops) { build(:project_auto_devops, project: project) }
+
+ it 'should not create a deploy token' do
+ expect do
+ auto_devops.save
+ end.not_to change { DeployToken.count }
+ end
+ end
+
+ context 'when the project already has a revoked gitlab-deploy-token' do
+ let(:project) { create(:project, :repository, :internal) }
+ let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :expired, projects: [project]) }
+ let(:auto_devops) { build(:project_auto_devops, project: project) }
+
+ it 'should not create a deploy token' do
+ expect do
+ auto_devops.save
+ end.not_to change { DeployToken.count }
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 54ef0be67ff..c3b4eb17a5c 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe JiraService do
include Gitlab::Routing
+ include AssetsHelpers
describe '#options' do
let(:service) do
@@ -164,6 +165,8 @@ describe JiraService do
it "creates Remote Link reference in JIRA for comment" do
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}"
+
# Creates comment
expect(WebMock).to have_requested(:post, @comment_url)
# Creates Remote Link in JIRA issue fields
@@ -173,7 +176,7 @@ describe JiraService do
object: {
url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}",
title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.",
- icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
+ icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: true }
}
)
@@ -464,4 +467,18 @@ describe JiraService do
end
end
end
+
+ describe 'favicon urls', :request_store do
+ it 'includes the standard favicon' do
+ props = described_class.new.send(:build_remote_link_props, url: 'http://example.com', title: 'title')
+ expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/assets/favicon(?:-\h+).png$}
+ end
+
+ it 'includes returns the custom favicon' do
+ create :appearance, favicon: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'))
+
+ props = described_class.new.send(:build_remote_link_props, url: 'http://example.com', title: 'title')
+ expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/uploads/-/system/appearance/favicon/\d+/favicon_main_dk.png$}
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 52fc7423c26..1a6ad3edd78 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -960,7 +960,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(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff'])
+ 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, ico'])
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 09dfeae6377..097144d04ce 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1260,7 +1260,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(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff'])
+ 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, ico'])
end
end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index c981a10ac38..57d238ff79b 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -816,6 +816,18 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(job.reload.trace.raw).to eq 'BUILD TRACE'
end
+
+ context 'when running state is sent' do
+ it 'updates update_at value' do
+ expect { update_job_after_time }.to change { job.reload.updated_at }
+ end
+ end
+
+ context 'when other state is sent' do
+ it "doesn't update update_at value" do
+ expect { update_job_after_time(20.minutes, state: 'success') }.not_to change { job.reload.updated_at }
+ end
+ end
end
context 'when job has been erased' do
@@ -838,6 +850,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
expect(response).to have_gitlab_http_status(403)
+ expect(response.header['Job-Status']).to eq 'failed'
expect(job.trace.raw).to eq 'Job failed'
expect(job).to be_failed
end
@@ -847,6 +860,12 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
new_params = params.merge(token: token)
put api("/jobs/#{job.id}"), new_params
end
+
+ def update_job_after_time(update_interval = 20.minutes, state = 'running')
+ Timecop.travel(job.updated_at + update_interval) do
+ update_job(job.token, state: state)
+ end
+ end
end
describe 'PATCH /api/v4/jobs/:id/trace' do
@@ -979,6 +998,17 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
end
+
+ context 'when the job is canceled' do
+ before do
+ job.cancel
+ patch_the_trace
+ end
+
+ it 'receives status in header' do
+ expect(response.header['Job-Status']).to eq 'canceled'
+ end
+ end
end
context 'when Runner makes a force-patch' do
diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb
index 98cd15e248b..52459cd369d 100644
--- a/spec/serializers/build_serializer_spec.rb
+++ b/spec/serializers/build_serializer_spec.rb
@@ -39,7 +39,7 @@ describe BuildSerializer do
expect(subject[:label]).to eq('failed')
expect(subject[:tooltip]).to eq('failed <br> (unknown failure)')
expect(subject[:icon]).to eq(status.icon)
- expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
+ expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png")
end
end
@@ -54,7 +54,7 @@ describe BuildSerializer do
expect(subject[:label]).to eq('passed')
expect(subject[:tooltip]).to eq('passed')
expect(subject[:icon]).to eq(status.icon)
- expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
+ expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png")
end
end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index e0e6eecb300..eb4235e3ee6 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -179,7 +179,7 @@ describe PipelineSerializer do
expect(subject[:text]).to eq(status.text)
expect(subject[:label]).to eq(status.label)
expect(subject[:icon]).to eq(status.icon)
- expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico")
+ expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png")
end
end
end
diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
index 559475e571c..0b010ebd507 100644
--- a/spec/serializers/status_entity_spec.rb
+++ b/spec/serializers/status_entity_spec.rb
@@ -18,17 +18,7 @@ describe StatusEntity do
it 'contains status details' do
expect(subject).to include :text, :icon, :favicon, :label, :group, :tooltip
expect(subject).to include :has_details, :details_path
- expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.ico')
- end
-
- it 'contains a dev namespaced favicon if dev env' do
- allow(Rails.env).to receive(:development?) { true }
- expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/dev/favicon_status_success.ico')
- end
-
- it 'contains a canary namespaced favicon if canary env' do
- stub_env('CANARY', 'true')
- expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/canary/favicon_status_success.ico')
+ expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.png')
end
end
end
diff --git a/spec/services/notification_recipient_service_spec.rb b/spec/services/notification_recipient_service_spec.rb
new file mode 100644
index 00000000000..340d4585e0c
--- /dev/null
+++ b/spec/services/notification_recipient_service_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe NotificationRecipientService do
+ let(:service) { described_class }
+ let(:assignee) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:other_projects) { create_list(:project, 5, :public) }
+
+ describe '#build_new_note_recipients' do
+ let(:issue) { create(:issue, project: project, assignees: [assignee]) }
+ let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id) }
+
+ def create_watcher
+ watcher = create(:user)
+ create(:notification_setting, project: project, user: watcher, level: :watch)
+
+ other_projects.each do |other_project|
+ create(:notification_setting, project: other_project, user: watcher, level: :watch)
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ create_watcher
+
+ service.build_new_note_recipients(note)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ service.build_new_note_recipients(note)
+ end
+
+ create_watcher
+
+ expect { service.build_new_note_recipients(note) }.not_to exceed_query_limit(control_count)
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 831678b949d..0eadc83bfe3 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -59,6 +59,20 @@ describe NotificationService, :mailer do
should_email(participant)
end
+
+ context 'for subgroups', :nested_groups do
+ before do
+ build_group(project)
+ end
+
+ it 'emails the participant' do
+ create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: @pg_participant)
+
+ notification_trigger
+
+ should_email_nested_group_user(@pg_participant)
+ end
+ end
end
shared_examples 'participating by assignee notification' do
@@ -239,34 +253,56 @@ describe NotificationService, :mailer do
end
describe 'new note on issue in project that belongs to a group' do
- let(:group) { create(:group) }
-
before do
note.project.namespace_id = group.id
- note.project.group.add_user(@u_watcher, GroupMember::MASTER)
- note.project.group.add_user(@u_custom_global, GroupMember::MASTER)
+ group.add_user(@u_watcher, GroupMember::MASTER)
+ group.add_user(@u_custom_global, GroupMember::MASTER)
note.project.save
@u_watcher.notification_settings_for(note.project).participating!
- @u_watcher.notification_settings_for(note.project.group).global!
+ @u_watcher.notification_settings_for(group).global!
update_custom_notification(:new_note, @u_custom_global)
reset_delivered_emails!
end
- it do
- notification.new_note(note)
+ shared_examples 'new note notifications' do
+ it do
+ notification.new_note(note)
+
+ should_email(note.noteable.author)
+ should_email(note.noteable.assignees.first)
+ should_email(@u_mentioned)
+ should_email(@u_custom_global)
+ should_not_email(@u_guest_custom)
+ should_not_email(@u_guest_watcher)
+ should_not_email(@u_watcher)
+ should_not_email(note.author)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+ end
- should_email(note.noteable.author)
- should_email(note.noteable.assignees.first)
- should_email(@u_mentioned)
- should_email(@u_custom_global)
- should_not_email(@u_guest_custom)
- should_not_email(@u_guest_watcher)
- should_not_email(@u_watcher)
- should_not_email(note.author)
- should_not_email(@u_participating)
- should_not_email(@u_disabled)
- should_not_email(@u_lazy_participant)
+ let(:group) { create(:group) }
+
+ it_behaves_like 'new note notifications'
+
+ context 'which is a subgroup', :nested_groups do
+ let!(:parent) { create(:group) }
+ let!(:group) { create(:group, parent: parent) }
+
+ it_behaves_like 'new note notifications'
+
+ it 'overrides child objects with global level' do
+ user = create(:user)
+ parent.add_developer(user)
+ user.notification_settings_for(parent).watch!
+ reset_delivered_emails!
+
+ notification.new_note(note)
+
+ should_email(user)
+ end
end
end
end
@@ -301,6 +337,31 @@ describe NotificationService, :mailer do
should_email(member)
should_email(admin)
end
+
+ context 'on project that belongs to subgroup', :nested_groups do
+ let(:group_reporter) { create(:user) }
+ let(:group_guest) { create(:user) }
+ let(:parent_group) { create(:group) }
+ let(:child_group) { create(:group, parent: parent_group) }
+ let(:project) { create(:project, namespace: child_group) }
+
+ context 'when user is group guest member' do
+ before do
+ parent_group.add_reporter(group_reporter)
+ parent_group.add_guest(group_guest)
+ group_guest.notification_settings_for(parent_group).watch!
+ group_reporter.notification_settings_for(parent_group).watch!
+ reset_delivered_emails!
+ end
+
+ it 'does not email guest user' do
+ notification.new_note(note)
+
+ should_email(group_reporter)
+ should_not_email(group_guest)
+ end
+ end
+ end
end
context 'issue note mention' do
@@ -311,6 +372,7 @@ describe NotificationService, :mailer do
before do
build_team(note.project)
+ build_group(note.project)
note.project.add_master(note.author)
add_users_with_subscription(note.project, issue)
reset_delivered_emails!
@@ -336,10 +398,20 @@ describe NotificationService, :mailer do
should_email(@u_guest_watcher)
should_email(note.noteable.author)
should_email(note.noteable.assignees.first)
- should_not_email(note.author)
+ should_email_nested_group_user(@pg_watcher)
should_email(@u_mentioned)
- should_not_email(@u_disabled)
should_email(@u_not_mentioned)
+ should_not_email(note.author)
+ should_not_email(@u_disabled)
+ should_not_email_nested_group_user(@pg_disabled)
+ end
+
+ it 'notifies parent group members with mention level', :nested_groups do
+ note = create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: "@#{@pg_mention.username}")
+
+ notification.new_note(note)
+
+ should_email_nested_group_user(@pg_mention)
end
it 'filters out "mentioned in" notes' do
@@ -352,17 +424,18 @@ describe NotificationService, :mailer do
end
context 'project snippet note' do
- let(:project) { create(:project, :public) }
+ let!(:project) { create(:project, :public) }
let(:snippet) { create(:project_snippet, project: project, author: create(:user)) }
- let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: snippet.project.id, note: '@all mentioned') }
+ let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: project.id, note: '@all mentioned') }
before do
- build_team(note.project)
+ build_team(project)
+ build_group(project)
# make sure these users can read the project snippet!
project.add_guest(@u_guest_watcher)
project.add_guest(@u_guest_custom)
-
+ add_member_for_parent_group(@pg_watcher, project)
note.project.add_master(note.author)
reset_delivered_emails!
end
@@ -370,7 +443,6 @@ describe NotificationService, :mailer do
describe '#new_note' do
it 'notifies the team members' do
notification.new_note(note)
-
# Notify all team members
note.project.team.members.each do |member|
# User with disabled notification should not be notified
@@ -449,6 +521,7 @@ describe NotificationService, :mailer do
before do
build_team(note.project)
+ build_group(project)
reset_delivered_emails!
allow(note.noteable).to receive(:author).and_return(@u_committer)
update_custom_notification(:new_note, @u_guest_custom, resource: project)
@@ -463,11 +536,13 @@ describe NotificationService, :mailer do
should_email(@u_guest_custom)
should_email(@u_committer)
should_email(@u_watcher)
+ should_email_nested_group_user(@pg_watcher)
should_not_email(@u_mentioned)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
+ should_not_email_nested_group_user(@pg_disabled)
end
it do
@@ -478,10 +553,12 @@ describe NotificationService, :mailer do
should_email(@u_committer)
should_email(@u_watcher)
should_email(@u_mentioned)
+ should_email_nested_group_user(@pg_watcher)
should_not_email(note.author)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
+ should_not_email_nested_group_user(@pg_disabled)
end
it do
@@ -548,10 +625,13 @@ describe NotificationService, :mailer do
should_email(@g_global_watcher)
should_email(@g_watcher)
should_email(@unsubscribed_mentioned)
+ should_email_nested_group_user(@pg_watcher)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
+ should_not_email_nested_group_user(@pg_disabled)
+ should_not_email_nested_group_user(@pg_mention)
end
it do
@@ -1922,19 +2002,69 @@ describe NotificationService, :mailer do
# Users in the project's group but not part of project's team
# with different notification settings
def build_group(project)
- group = create(:group, :public)
- project.group = group
+ group = create_nested_group
+ project.update(namespace_id: group.id)
# Group member: global=disabled, group=watch
- @g_watcher = create_user_with_notification(:watch, 'group_watcher', project.group)
+ @g_watcher ||= create_user_with_notification(:watch, 'group_watcher', project.group)
@g_watcher.notification_settings_for(nil).disabled!
# Group member: global=watch, group=global
- @g_global_watcher = create_global_setting_for(create(:user), :watch)
+ @g_global_watcher ||= create_global_setting_for(create(:user), :watch)
group.add_users([@g_watcher, @g_global_watcher], :master)
+
group
end
+ # Creates a nested group only if supported
+ # to avoid errors on MySQL
+ def create_nested_group
+ if Group.supports_nested_groups?
+ parent_group = create(:group, :public)
+ child_group = create(:group, :public, parent: parent_group)
+
+ # Parent group member: global=disabled, parent_group=watch, child_group=global
+ @pg_watcher ||= create_user_with_notification(:watch, 'parent_group_watcher', parent_group)
+ @pg_watcher.notification_settings_for(nil).disabled!
+
+ # Parent group member: global=global, parent_group=disabled, child_group=global
+ @pg_disabled ||= create_user_with_notification(:disabled, 'parent_group_disabled', parent_group)
+ @pg_disabled.notification_settings_for(nil).global!
+
+ # Parent group member: global=global, parent_group=mention, child_group=global
+ @pg_mention ||= create_user_with_notification(:mention, 'parent_group_mention', parent_group)
+ @pg_mention.notification_settings_for(nil).global!
+
+ # Parent group member: global=global, parent_group=participating, child_group=global
+ @pg_participant ||= create_user_with_notification(:participating, 'parent_group_participant', parent_group)
+ @pg_mention.notification_settings_for(nil).global!
+
+ child_group
+ else
+ create(:group, :public)
+ end
+ end
+
+ def add_member_for_parent_group(user, project)
+ return unless Group.supports_nested_groups?
+
+ project.reload
+
+ project.group.parent.add_master(user)
+ end
+
+ def should_email_nested_group_user(user, times: 1, recipients: email_recipients)
+ return unless Group.supports_nested_groups?
+
+ should_email(user, times: 1, recipients: email_recipients)
+ end
+
+ def should_not_email_nested_group_user(user, recipients: email_recipients)
+ return unless Group.supports_nested_groups?
+
+ should_not_email(user, recipients: email_recipients)
+ end
+
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index e28b0ea5cf2..57d081cffb3 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe SystemNoteService do
include Gitlab::Routing
include RepoHelpers
+ include AssetsHelpers
set(:group) { create(:group) }
set(:project) { create(:project, :repository, group: group) }
@@ -769,6 +770,8 @@ describe SystemNoteService do
end
describe "new reference" do
+ let(:favicon_path) { "http://localhost/assets/#{find_asset('favicon.png').digest_path}" }
+
before do
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
end
@@ -789,7 +792,7 @@ describe SystemNoteService do
object: {
url: project_commit_url(project, commit),
title: "GitLab: Mentioned on commit - #{commit.title}",
- icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
+ icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
)
@@ -815,7 +818,7 @@ describe SystemNoteService do
object: {
url: project_issue_url(project, issue),
title: "GitLab: Mentioned on issue - #{issue.title}",
- icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
+ icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
)
@@ -841,7 +844,7 @@ describe SystemNoteService do
object: {
url: project_snippet_url(project, snippet),
title: "GitLab: Mentioned on snippet - #{snippet.title}",
- icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
+ icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
)
diff --git a/spec/support/helpers/assets_helpers.rb b/spec/support/helpers/assets_helpers.rb
new file mode 100644
index 00000000000..09bbf451671
--- /dev/null
+++ b/spec/support/helpers/assets_helpers.rb
@@ -0,0 +1,15 @@
+module AssetsHelpers
+ # In a CI environment the assets are not compiled, as there is a CI job
+ # `compile-assets` that compiles them in the prepare stage for all following
+ # specs.
+ # Locally the assets are precompiled dynamically.
+ #
+ # Sprockets doesn't provide one method to access an asset for both cases.
+ def find_asset(asset_name)
+ if ENV['CI']
+ Sprockets::Railtie.build_environment(Rails.application, true)[asset_name]
+ else
+ Rails.application.assets.find_asset(asset_name)
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
index 35e451b2f9a..233076ad6fa 100644
--- a/spec/tasks/gitlab/storage_rake_spec.rb
+++ b/spec/tasks/gitlab/storage_rake_spec.rb
@@ -1,6 +1,6 @@
require 'rake_helper'
-describe 'gitlab:storage:*' do
+describe 'rake gitlab:storage:*' do
before do
Rake.application.rake_require 'tasks/gitlab/storage'
@@ -44,16 +44,18 @@ describe 'gitlab:storage:*' do
end
describe 'gitlab:storage:migrate_to_hashed' do
+ let(:task) { 'gitlab:storage:migrate_to_hashed' }
+
context '0 legacy projects' do
it 'does nothing' do
expect(StorageMigratorWorker).not_to receive(:perform_async)
- run_rake_task('gitlab:storage:migrate_to_hashed')
+ run_rake_task(task)
end
end
context '3 legacy projects' do
- let(:projects) { create_list(:project, 3, storage_version: 0) }
+ let(:projects) { create_list(:project, 3, :legacy_storage) }
context 'in batches of 1' do
before do
@@ -65,7 +67,7 @@ describe 'gitlab:storage:*' do
expect(StorageMigratorWorker).to receive(:perform_async).with(project.id, project.id)
end
- run_rake_task('gitlab:storage:migrate_to_hashed')
+ run_rake_task(task)
end
end
@@ -80,23 +82,48 @@ describe 'gitlab:storage:*' do
expect(StorageMigratorWorker).to receive(:perform_async).with(first, last)
end
- run_rake_task('gitlab:storage:migrate_to_hashed')
+ run_rake_task(task)
end
end
end
+
+ context 'with same id in range' do
+ it 'displays message when project cant be found' do
+ stub_env('ID_FROM', 99999)
+ stub_env('ID_TO', 99999)
+
+ expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=99999/).to_stdout
+ end
+
+ it 'displays a message when project exists but its already migrated' do
+ project = create(:project)
+ stub_env('ID_FROM', project.id)
+ stub_env('ID_TO', project.id)
+
+ expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=#{project.id}/).to_stdout
+ end
+
+ it 'enqueues migration when project can be found' do
+ project = create(:project, :legacy_storage)
+ stub_env('ID_FROM', project.id)
+ stub_env('ID_TO', project.id)
+
+ expect { run_rake_task(task) }.to output(/Enqueueing storage migration .* \(ID=#{project.id}\)/).to_stdout
+ end
+ end
end
describe 'gitlab:storage:legacy_projects' do
it_behaves_like 'rake entities summary', 'projects', 'Legacy' do
let(:task) { 'gitlab:storage:legacy_projects' }
- let(:create_collection) { create_list(:project, 3, storage_version: 0) }
+ let(:create_collection) { create_list(:project, 3, :legacy_storage) }
end
end
describe 'gitlab:storage:list_legacy_projects' do
it_behaves_like 'rake listing entities', 'projects', 'Legacy' do
let(:task) { 'gitlab:storage:list_legacy_projects' }
- let(:create_collection) { create_list(:project, 3, storage_version: 0) }
+ let(:create_collection) { create_list(:project, 3, :legacy_storage) }
end
end
@@ -133,7 +160,7 @@ describe 'gitlab:storage:*' do
describe 'gitlab:storage:hashed_attachments' do
it_behaves_like 'rake entities summary', 'attachments', 'Hashed' do
let(:task) { 'gitlab:storage:hashed_attachments' }
- let(:project) { create(:project, storage_version: 2) }
+ let(:project) { create(:project) }
let(:create_collection) { create_list(:upload, 3, model: project) }
end
end
@@ -141,7 +168,7 @@ describe 'gitlab:storage:*' do
describe 'gitlab:storage:list_hashed_attachments' do
it_behaves_like 'rake listing entities', 'attachments', 'Hashed' do
let(:task) { 'gitlab:storage:list_hashed_attachments' }
- let(:project) { create(:project, storage_version: 2) }
+ let(:project) { create(:project) }
let(:create_collection) { create_list(:upload, 3, model: project) }
end
end
diff --git a/spec/uploaders/favicon_uploader_spec.rb b/spec/uploaders/favicon_uploader_spec.rb
new file mode 100644
index 00000000000..db8a3207f4d
--- /dev/null
+++ b/spec/uploaders/favicon_uploader_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+RSpec.describe FaviconUploader do
+ include CarrierWave::Test::Matchers
+
+ let(:uploader) { described_class.new(build_stubbed(:user)) }
+
+ after do
+ uploader.remove!
+ end
+
+ def upload_fixture(filename)
+ fixture_file_upload(Rails.root.join('spec', 'fixtures', filename))
+ end
+
+ context 'versions' do
+ before do
+ uploader.store!(upload_fixture('dk.png'))
+ end
+
+ it 'has the correct format' do
+ expect(uploader.favicon_main).to be_format('png')
+ end
+
+ it 'has the correct dimensions' do
+ expect(uploader.favicon_main).to have_dimensions(32, 32)
+ end
+ end
+end
diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
index d15391911c1..cb1b9e6f5fb 100644
--- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
+++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
@@ -12,7 +12,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
it 'shows warning message' do
render
- expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_css('.auto-devops-warning-message')
expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a')
expect(rendered).to have_link('Kubernetes cluster')
end
@@ -26,7 +26,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
it 'shows warning message' do
render
- expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_css('.auto-devops-warning-message')
expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a')
expect(rendered).to have_link('Kubernetes cluster')
end
@@ -42,7 +42,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
it 'shows warning message' do
render
- expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_css('.auto-devops-warning-message')
expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
end
end
@@ -55,7 +55,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
it 'does not show warning message' do
render
- expect(rendered).not_to have_css('.settings-message')
+ expect(rendered).not_to have_css('.auto-devops-warning-message')
end
end
end
diff --git a/spec/workers/storage_migrator_worker_spec.rb b/spec/workers/storage_migrator_worker_spec.rb
index ff625164142..815432aacce 100644
--- a/spec/workers/storage_migrator_worker_spec.rb
+++ b/spec/workers/storage_migrator_worker_spec.rb
@@ -2,29 +2,24 @@ require 'spec_helper'
describe StorageMigratorWorker do
subject(:worker) { described_class.new }
- let(:projects) { create_list(:project, 2, :legacy_storage) }
+ let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) }
+ let(:ids) { projects.map(&:id) }
describe '#perform' do
- let(:ids) { projects.map(&:id) }
+ it 'delegates to MigratorService' do
+ expect_any_instance_of(Gitlab::HashedStorage::Migrator).to receive(:bulk_migrate).with(5, 10)
- it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do
- expect(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice
-
- worker.perform(ids.min, ids.max)
+ worker.perform(5, 10)
end
- it 'sets projects as read only' do
- allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice
- worker.perform(ids.min, ids.max)
+ it 'migrates projects in the specified range' do
+ Sidekiq::Testing.inline! do
+ worker.perform(ids.min, ids.max)
+ end
projects.each do |project|
- expect(project.reload.repository_read_only?).to be_truthy
+ expect(project.reload.hashed_storage?(:attachments)).to be_truthy
end
end
-
- it 'rescues and log exceptions' do
- allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError)
- expect { worker.perform(ids.min, ids.max) }.not_to raise_error
- end
end
end