Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml14
-rw-r--r--.gitlab/ci/memory.gitlab-ci.yml12
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml88
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml17
-rw-r--r--CHANGELOG.md26
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js1
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue10
-rw-r--r--app/assets/javascripts/boards/index.js1
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js2
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js7
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js6
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue4
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions_dropdown.vue16
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue11
-rw-r--r--app/assets/javascripts/diffs/components/diff_gutter_avatars.vue2
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js6
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js6
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js3
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js26
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue7
-rw-r--r--app/assets/javascripts/ide/stores/actions.js16
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js9
-rw-r--r--app/assets/javascripts/ide/stores/utils.js4
-rw-r--r--app/assets/javascripts/ide/utils.js2
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue25
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/locked_warning.vue7
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js17
-rw-r--r--app/assets/javascripts/manual_ordering.js58
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue3
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue112
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue50
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js12
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js21
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js7
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js3
-rw-r--r--app/assets/javascripts/pages/dashboard/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js2
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue49
-rw-r--r--app/assets/javascripts/repository/graphql.js17
-rw-r--r--app/assets/javascripts/repository/index.js1
-rw-r--r--app/assets/javascripts/repository/log_tree.js64
-rw-r--r--app/assets/javascripts/repository/queries/getCommit.query.graphql10
-rw-r--r--app/assets/javascripts/repository/queries/getCommits.query.graphql10
-rw-r--r--app/assets/javascripts/repository/queries/getFiles.query.graphql1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue5
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js4
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js2
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js3
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment.js50
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/constants.js8
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/index.js16
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/login.js3
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/note.js14
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/utils.js6
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/wrapper.js36
-rw-r--r--app/assets/javascripts/visual_review_toolbar/index.js9
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/state.js3
-rw-r--r--app/assets/javascripts/visual_review_toolbar/styles/toolbar.css48
-rw-r--r--app/assets/stylesheets/pages/issuable.scss6
-rw-r--r--app/assets/stylesheets/pages/issues.scss14
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/controllers/concerns/continue_params.rb2
-rw-r--r--app/controllers/concerns/internal_redirect.rb4
-rw-r--r--app/controllers/concerns/requires_whitelisted_monitoring_client.rb4
-rw-r--r--app/controllers/groups_controller.rb4
-rw-r--r--app/controllers/projects/forks_controller.rb18
-rw-r--r--app/controllers/projects/imports_controller.rb8
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/jobs_controller.rb2
-rw-r--r--app/controllers/projects/refs_controller.rb1
-rw-r--r--app/controllers/registrations_controller.rb30
-rw-r--r--app/helpers/application_settings_helper.rb5
-rw-r--r--app/helpers/boards_helper.rb3
-rw-r--r--app/helpers/issuables_helper.rb3
-rw-r--r--app/helpers/issues_helper.rb1
-rw-r--r--app/helpers/recaptcha_experiment_helper.rb7
-rw-r--r--app/helpers/search_helper.rb5
-rw-r--r--app/models/application_setting.rb13
-rw-r--r--app/models/application_setting_implementation.rb22
-rw-r--r--app/models/broadcast_message.rb2
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/models/namespace.rb2
-rw-r--r--app/models/namespace/aggregation_schedule.rb7
-rw-r--r--app/models/namespace/root_storage_statistics.rb10
-rw-r--r--app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb10
-rw-r--r--app/services/projects/propagate_service_template.rb2
-rw-r--r--app/views/admin/application_settings/_localization.html.haml7
-rw-r--r--app/views/admin/application_settings/_logging.html.haml38
-rw-r--r--app/views/admin/application_settings/_spam.html.haml5
-rw-r--r--app/views/admin/application_settings/reporting.html.haml11
-rw-r--r--app/views/admin/runners/index.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml40
-rw-r--r--app/views/projects/issues/_issues.html.haml2
-rw-r--r--app/views/projects/new.html.haml4
-rw-r--r--app/views/projects/pages_domains/_form.html.haml2
-rw-r--r--app/views/shared/_issues.html.haml2
-rw-r--r--app/views/shared/_personal_access_tokens_created_container.html.haml7
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml14
-rw-r--r--app/views/shared/_personal_access_tokens_table.html.haml23
-rw-r--r--app/views/shared/boards/components/sidebar/_time_tracker.html.haml1
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml3
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml6
-rw-r--r--app/views/shared/tokens/_scopes_list.html.haml2
-rw-r--r--app/workers/pages_domain_ssl_renewal_cron_worker.rb4
-rw-r--r--app/workers/pages_domain_ssl_renewal_worker.rb6
-rw-r--r--changelogs/unreleased/30355-use-hours-only-for-time-tracking.yml5
-rw-r--r--changelogs/unreleased/51952-forking-via-webide.yml5
-rw-r--r--changelogs/unreleased/58802-rename-webide.yml5
-rw-r--r--changelogs/unreleased/59702-fix-notification-flags-for-ms-teams.yml5
-rw-r--r--changelogs/unreleased/60860-keep-empty-folders-in-tree.yml5
-rw-r--r--changelogs/unreleased/60879-fix-reports-timing-out.yml5
-rw-r--r--changelogs/unreleased/62938-wcag-aa-edited-text-color.yml5
-rw-r--r--changelogs/unreleased/63247-add-conf-toast-and-link.yml5
-rw-r--r--changelogs/unreleased/63513-ensure-gitlab-jsoncache-includes-the-gitlab-version-in-the-cache-key.yml5
-rw-r--r--changelogs/unreleased/always-allow-prometheus-access-in-dev.yml5
-rw-r--r--changelogs/unreleased/always-display-environment-selector.yml5
-rw-r--r--changelogs/unreleased/bug-63162-duplicate_path_in_links.yml5
-rw-r--r--changelogs/unreleased/fe-issue-reorder.yml5
-rw-r--r--changelogs/unreleased/fix-jupyter-git-v3.yml5
-rw-r--r--changelogs/unreleased/fix-labels-in-hooks.yml5
-rw-r--r--changelogs/unreleased/fix-notes-emails-with-group-settings.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.49.0.yml5
-rw-r--r--changelogs/unreleased/mh-colon-autocomplete.yml5
-rw-r--r--changelogs/unreleased/refactor-sentry.yml5
-rw-r--r--changelogs/unreleased/require-pipeline-when-enabling-only-allow-merge-if-pipeline-succeeds.yml5
-rw-r--r--changelogs/unreleased/sh-cache-negative-entries-find-commit.yml5
-rw-r--r--changelogs/unreleased/sh-omit-issues-links-on-poll.yml5
-rw-r--r--changelogs/unreleased/sh-service-template-bug.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-6-0.yml5
-rw-r--r--config/initializers/0_inflections.rb8
-rw-r--r--config/initializers/1_settings.rb2
-rw-r--r--config/initializers/forbid_sidekiq_in_transactions.rb2
-rw-r--r--config/initializers/sentry.rb13
-rw-r--r--db/migrate/20190402150158_backport_enterprise_schema.rb48
-rw-r--r--db/migrate/20190513174947_enable_create_incident_issues_by_default.rb19
-rw-r--r--db/migrate/20190531153110_create_namespace_root_storage_statistics.rb22
-rw-r--r--db/migrate/20190605184422_create_namespace_aggregation_schedules.rb14
-rw-r--r--db/migrate/20190611090827_add_time_tracking_limit_to_hours_to_application_settings.rb21
-rw-r--r--db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb38
-rw-r--r--db/schema.rb30
-rw-r--r--doc/administration/high_availability/gitlab.md11
-rw-r--r--doc/administration/high_availability/pgbouncer.md27
-rw-r--r--doc/administration/operations/extra_sidekiq_processes.md199
-rw-r--r--doc/api/settings.md5
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/docker/using_docker_build.md9
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/api_graphql_styleguide.md2
-rw-r--r--doc/development/code_comments.md14
-rw-r--r--doc/development/documentation/styleguide.md21
-rw-r--r--doc/development/testing_guide/frontend_testing.md18
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md10
-rw-r--r--doc/user/application_security/dependency_scanning/index.md17
-rw-r--r--doc/user/operations_dashboard/index.md11
-rw-r--r--doc/user/profile/account/two_factor_authentication.md5
-rw-r--r--doc/user/project/clusters/index.md7
-rw-r--r--doc/user/project/integrations/prometheus.md2
-rw-r--r--doc/user/project/merge_requests/merge_when_pipeline_succeeds.md17
-rw-r--r--doc/user/project/pages/getting_started_part_three.md39
-rw-r--r--doc/workflow/time_tracking.md10
-rw-r--r--lib/after_commit_queue.rb14
-rw-r--r--lib/api/settings.rb8
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml2
-rw-r--r--lib/gitlab/database.rb18
-rw-r--r--lib/gitlab/git/repository.rb5
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb40
-rw-r--r--lib/gitlab/gon_helper.rb4
-rw-r--r--lib/gitlab/graphql/authorize/authorize_resource.rb12
-rw-r--r--lib/gitlab/json_cache.rb2
-rw-r--r--lib/gitlab/lets_encrypt.rb16
-rw-r--r--lib/gitlab/lets_encrypt/client.rb10
-rw-r--r--lib/gitlab/sentry.rb2
-rw-r--r--lib/gitlab/time_tracking_formatter.rb10
-rw-r--r--lib/tasks/gitlab/backup.rake4
-rw-r--r--locale/gitlab.pot179
-rw-r--r--package.json2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb8
-rwxr-xr-xscripts/generate-gems-memory-metrics-static18
-rwxr-xr-xscripts/generate-gems-size-metrics-static30
-rwxr-xr-xscripts/memory-static20
-rwxr-xr-xscripts/memory-static-objects27
-rwxr-xr-xscripts/review_apps/review-apps.sh102
-rwxr-xr-xscripts/trigger-build9
-rw-r--r--spec/controllers/concerns/continue_params_spec.rb8
-rw-r--r--spec/controllers/concerns/internal_redirect_spec.rb77
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb16
-rw-r--r--spec/controllers/registrations_controller_spec.rb17
-rw-r--r--spec/factories/namespace/aggregation_schedules.rb7
-rw-r--r--spec/factories/namespace/root_storage_statistics.rb7
-rw-r--r--spec/factories/namespaces.rb8
-rw-r--r--spec/features/boards/sidebar_spec.rb22
-rw-r--r--spec/features/groups/issues_spec.rb59
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb70
-rw-r--r--spec/features/projects/environments/environment_metrics_spec.rb39
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb40
-rw-r--r--spec/features/raven_js_spec.rb2
-rw-r--r--spec/features/users/signup_spec.rb2
-rw-r--r--spec/frontend/boards/modal_store_spec.js2
-rw-r--r--spec/frontend/boards/services/board_service_spec.js390
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js16
-rw-r--r--spec/frontend/helpers/vuex_action_helper.js7
-rw-r--r--spec/frontend/helpers/vuex_action_helper_spec.js166
-rw-r--r--spec/frontend/ide/utils_spec.js44
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js6
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap14
-rw-r--r--spec/frontend/repository/components/table/row_spec.js2
-rw-r--r--spec/frontend/repository/log_tree_spec.js129
-rw-r--r--spec/frontend/test_setup.js6
-rw-r--r--spec/helpers/recaptcha_experiment_helper_spec.rb23
-rw-r--r--spec/helpers/search_helper_spec.rb6
-rw-r--r--spec/javascripts/boards/boards_store_spec.js10
-rw-r--r--spec/javascripts/boards/components/issue_time_estimate_spec.js70
-rw-r--r--spec/javascripts/filtered_search/visual_token_value_spec.js2
-rw-r--r--spec/javascripts/ide/components/ide_tree_list_spec.js14
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js37
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js37
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js11
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js96
-rw-r--r--spec/javascripts/monitoring/mock_data.js13
-rw-r--r--spec/javascripts/monitoring/store/actions_spec.js17
-rw-r--r--spec/javascripts/monitoring/store/mutations_spec.js15
-rw-r--r--spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js25
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb18
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb13
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb51
-rw-r--r--spec/lib/gitlab/json_cache_spec.rb35
-rw-r--r--spec/lib/gitlab/lets_encrypt/client_spec.rb36
-rw-r--r--spec/lib/gitlab/lets_encrypt_spec.rb56
-rw-r--r--spec/lib/gitlab/time_tracking_formatter_spec.rb43
-rw-r--r--spec/migrations/backport_enterprise_schema_spec.rb41
-rw-r--r--spec/models/application_setting_spec.rb30
-rw-r--r--spec/models/broadcast_message_spec.rb6
-rw-r--r--spec/models/internal_id_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb2
-rw-r--r--spec/models/namespace/aggregation_schedule_spec.rb7
-rw-r--r--spec/models/namespace/root_storage_statistics_spec.rb10
-rw-r--r--spec/models/namespace_spec.rb2
-rw-r--r--spec/requests/api/helpers_spec.rb6
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb12
-rw-r--r--spec/services/projects/propagate_service_template_spec.rb2
-rw-r--r--spec/services/system_note_service_spec.rb24
-rw-r--r--spec/spec_helper.rb6
-rw-r--r--spec/support/helpers/prometheus_helpers.rb4
-rw-r--r--spec/support/helpers/stub_configuration.rb6
-rw-r--r--spec/support/inspect_squelch.rb7
-rw-r--r--spec/support/shared_examples/application_setting_examples.rb39
-rw-r--r--spec/support/sidekiq.rb4
-rw-r--r--spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb13
-rw-r--r--spec/workers/pages_domain_ssl_renewal_worker_spec.rb19
-rw-r--r--vendor/jupyter/values.yaml2
-rw-r--r--yarn.lock38
267 files changed, 3650 insertions, 1120 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index b7ef4b31743..eb50f08c1a7 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -28,11 +28,15 @@
policy: pull
stage: test
-.dedicated-no-docs-pull-cache-job:
- extends: .dedicated-pull-cache-job
+.no-docs:
except:
- /(^docs[\/-].*|.*-docs$)/
+.dedicated-no-docs-pull-cache-job:
+ extends:
+ - .dedicated-pull-cache-job
+ - .no-docs
+
.dedicated-no-docs-and-no-qa-pull-cache-job:
extends: .dedicated-pull-cache-job
except:
@@ -45,6 +49,12 @@
variables:
SETUP_DB: "false"
+# Jobs that need a dedicated runner, with no cache
+.dedicated-no-docs:
+ extends:
+ - .dedicated-runner
+ - .no-docs
+
.single-script-job-dedicated-runner:
extends: .dedicated-runner
image: ruby:2.6-alpine
diff --git a/.gitlab/ci/memory.gitlab-ci.yml b/.gitlab/ci/memory.gitlab-ci.yml
index 50b843df585..2f3907a331a 100644
--- a/.gitlab/ci/memory.gitlab-ci.yml
+++ b/.gitlab/ci/memory.gitlab-ci.yml
@@ -5,15 +5,15 @@ memory-static:
# Loads each of gems in the Gemfile and checks how much memory they consume when they are required.
# 'derailed_benchmarks' internally uses 'get_process_mem'
- - scripts/memory-static 'tmp/memory_static_full_report.txt' 'tmp/memory_static_metrics.txt'
+ - bundle exec derailed bundle:mem > tmp/memory_bundle_mem.txt
+ - scripts/generate-gems-size-metrics-static tmp/memory_bundle_mem.txt >> 'tmp/memory_metrics.txt'
# Outputs detailed information about objects created while gems are loaded.
# 'derailed_benchmarks' internally uses 'memory_profiler'
- - scripts/memory-static-objects 'tmp/memory_static_objects_full_report.txt' 'tmp/memory_static_metrics.txt'
+ - bundle exec derailed bundle:objects > tmp/memory_bundle_objects.txt
+ - scripts/generate-gems-memory-metrics-static tmp/memory_bundle_objects.txt >> 'tmp/memory_metrics.txt'
artifacts:
paths:
- - tmp/memory_static_full_report.txt
- - tmp/memory_static_objects_full_report.txt
- - tmp/memory_static_metrics.txt
+ - tmp/memory_*.txt
reports:
- metrics: tmp/memory_static_metrics.txt
+ metrics: tmp/memory_metrics.txt
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index d0e09dbf2f8..89b5ae38072 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -1,98 +1,26 @@
include:
- template: Code-Quality.gitlab-ci.yml
+ - template: Security/SAST.gitlab-ci.yml
+ - template: Security/Dependency-Scanning.gitlab-ci.yml
code_quality:
- extends: .dedicated-no-docs-no-db-pull-cache-job
+ extends: .dedicated-no-docs
# gitlab-org runners set `privileged: false` but we need to have it set to true
# since we're using Docker in Docker
tags: []
before_script: []
cache: {}
dependencies: []
- variables:
- SETUP_DB: "false"
sast:
- extends: .dedicated-no-docs-no-db-pull-cache-job
- image: docker:stable
+ extends: .dedicated-no-docs
+ before_script: []
+ tags: []
variables:
SAST_CONFIDENCE_LEVEL: 2
DOCKER_DRIVER: overlay2
- allow_failure: true
- tags: []
- before_script: []
- cache: {}
- dependencies: []
- services:
- - docker:stable-dind
- script:
- - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
- function propagate_env_vars() {
- CURRENT_ENV=$(printenv)
-
- for VAR_NAME; do
- echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
- done
- }
- - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - |
- docker run \
- $(propagate_env_vars \
- SAST_ANALYZER_IMAGES \
- SAST_ANALYZER_IMAGE_PREFIX \
- SAST_ANALYZER_IMAGE_TAG \
- SAST_DEFAULT_ANALYZERS \
- SAST_BRAKEMAN_LEVEL \
- SAST_GOSEC_LEVEL \
- SAST_FLAWFINDER_LEVEL \
- SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
- SAST_RUN_ANALYZER_TIMEOUT \
- ) \
- --volume "$PWD:/code" \
- --volume /var/run/docker.sock:/var/run/docker.sock \
- "registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
- artifacts:
- reports:
- sast: gl-sast-report.json
dependency_scanning:
- extends: .dedicated-no-docs-no-db-pull-cache-job
- image: docker:stable
- variables:
- DOCKER_DRIVER: overlay2
- allow_failure: true
- tags: []
before_script: []
- cache: {}
- dependencies: []
- services:
- - docker:stable-dind
- script:
- - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - | # this is required to avoid undesirable reset of Docker image ENV variables being set on build stage
- function propagate_env_vars() {
- CURRENT_ENV=$(printenv)
-
- for VAR_NAME; do
- echo $CURRENT_ENV | grep "${VAR_NAME}=" > /dev/null && echo "--env $VAR_NAME "
- done
- }
- - |
- docker run \
- $(propagate_env_vars \
- DS_ANALYZER_IMAGES \
- DS_ANALYZER_IMAGE_PREFIX \
- DS_ANALYZER_IMAGE_TAG \
- DS_DEFAULT_ANALYZERS \
- DEP_SCAN_DISABLE_REMOTE_CHECKS \
- DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
- DS_PULL_ANALYZER_IMAGE_TIMEOUT \
- DS_RUN_ANALYZER_TIMEOUT \
- ) \
- --volume "$PWD:/code" \
- --volume /var/run/docker.sock:/var/run/docker.sock \
- "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
- artifacts:
- reports:
- dependency_scanning: gl-dependency-scanning-report.json
+ tags: []
+ extends: .dedicated-no-docs
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 9b764028be9..933af90c85a 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -77,6 +77,7 @@ schedule:review-build-cng:
.review-deploy-base: &review-deploy-base
<<: *review-base
allow_failure: true
+ retry: 2
stage: review
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
@@ -95,10 +96,16 @@ schedule:review-build-cng:
- install_api_client_dependencies_with_apk
- source scripts/review_apps/review-apps.sh
script:
- - perform_review_app_deployment
+ - check_kube_domain
+ - ensure_namespace
+ - install_tiller
+ - install_external_dns
+ - download_chart
+ - deploy || display_deployment_debug
+ - wait_for_review_app_to_be_accessible
+ - add_license
artifacts:
- paths:
- - review_app_url.txt
+ paths: [review_app_url.txt]
expire_in: 2 days
when: always
@@ -108,8 +115,6 @@ review-deploy:
schedule:review-deploy:
<<: *review-deploy-base
<<: *review-schedules-only
- script:
- - perform_review_app_deployment
review-stop:
<<: *review-base
@@ -124,11 +129,11 @@ review-stop:
script:
- source scripts/review_apps/review-apps.sh
- delete
- - cleanup
.review-qa-base: &review-qa-base
<<: *review-docker
allow_failure: true
+ retry: 2
stage: qa
variables:
<<: *review-docker-variables
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 118034867ad..8d4509e370d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,23 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 12.0.2 (2019-06-25)
+
+### Fixed (7 changes, 1 of them is from the community)
+
+- Fix missing API notification flags for Microsoft Teams. !29824 (Seiji Suenaga)
+- Fixed 'diff version changes' link not working. !29825
+- Fix label serialization in issue and note hooks. !29850
+- Include the GitLab version in the cache key for Gitlab::JsonCache. !29938
+- Prevent EE backport migrations from running if CE is not migrated. !30002
+- Silence backup warnings when CRON=1 in use. !30033
+- Fix comment emails not respecting group-level notification email.
+
+### Performance (1 change)
+
+- Omit issues links in merge request entity API response. !29917
+
+
## 12.0.1 (2019-06-24)
- No changes.
@@ -316,6 +333,15 @@ entry.
- Moves snowplow to CE repo.
+## 11.11.4 (2019-06-26)
+
+### Fixed (3 changes)
+
+- Fix Fogbugz Importer not working. !29383
+- Fix scrolling to top on assignee change. !29500
+- Fix IDE commit using latest ref in branch and overriding contents. !29769
+
+
## 11.11.3 (2019-06-10)
### Fixed (5 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 9db5ea12f52..7f3a46a841e 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.48.0
+1.49.0
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index c587b276fa3..2ace0060c42 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -38,6 +38,7 @@ export default Vue.extend({
issue: {},
list: {},
loadingAssignees: false,
+ timeTrackingLimitToHours: boardsStore.timeTracking.limitToHours,
};
},
computed: {
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
index 98c1d29db16..3385aad5b11 100644
--- a/app/assets/javascripts/boards/components/issue_time_estimate.vue
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -2,6 +2,7 @@
import { GlTooltip } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
+import boardsStore from '../stores/boards_store';
export default {
components: {
@@ -14,12 +15,17 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ limitToHours: boardsStore.timeTracking.limitToHours,
+ };
+ },
computed: {
title() {
- return stringifyTime(parseSeconds(this.estimate), true);
+ return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }), true);
},
timeEstimate() {
- return stringifyTime(parseSeconds(this.estimate));
+ return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }));
},
},
};
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index f2f37d22b97..a020765f335 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -49,6 +49,7 @@ export default () => {
}
boardsStore.create();
+ boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
issueBoardsApp = new Vue({
el: $boardApp,
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index 983b28d2e67..636ca99952c 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -1,7 +1,7 @@
/* global DocumentTouch */
import $ from 'jquery';
-import sortableConfig from '../../sortable/sortable_config';
+import sortableConfig from 'ee_else_ce/sortable/sortable_config';
export function sortableStart() {
$('.has-tooltip')
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 4b3b44574a8..4ba4cde6bae 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -12,6 +12,9 @@ import eventHub from '../eventhub';
const boardsStore = {
disabled: false,
+ timeTracking: {
+ limitToHours: false,
+ },
scopedLabels: {
helpLink: '',
enabled: false,
@@ -222,6 +225,10 @@ const boardsStore = {
setIssueDetail(issueDetail) {
this.detail.issue = issueDetail;
},
+
+ setTimeTrackingLimitToHours(limitToHours) {
+ this.timeTracking.limitToHours = parseBoolean(limitToHours);
+ },
};
BoardsStoreEE.initEESpecific(boardsStore);
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
index 17ea4d77795..6e632519d8a 100644
--- a/app/assets/javascripts/clusters/services/application_state_machine.js
+++ b/app/assets/javascripts/clusters/services/application_state_machine.js
@@ -80,6 +80,9 @@ const applicationStateMachine = {
installFailed: false,
},
},
+ [NOT_INSTALLABLE]: {
+ target: NOT_INSTALLABLE,
+ },
// This is possible in artificial environments for E2E testing
[INSTALLED]: {
target: INSTALLED,
@@ -108,6 +111,9 @@ const applicationStateMachine = {
updateSuccessful: false,
},
},
+ [NOT_INSTALLABLE]: {
+ target: NOT_INSTALLABLE,
+ },
[UNINSTALL_EVENT]: {
target: UNINSTALLING,
effects: {
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index d0cc4897aeb..a4394ab7e92 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -12,6 +12,7 @@ import 'core-js/es/promise/finally';
import 'core-js/es/string/code-point-at';
import 'core-js/es/string/from-code-point';
import 'core-js/es/string/includes';
+import 'core-js/es/string/starts-with';
import 'core-js/es/symbol';
import 'core-js/es/map';
import 'core-js/es/weak-map';
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index aaa9f8b759a..58d5b658b17 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -49,6 +49,8 @@ export default {
return this.author.id ? this.author.id : '';
},
authorUrl() {
+ // TODO: when the vue i18n rules are merged need to disable @gitlab/i18n/no-non-i18n-strings
+ // name: 'mailto:' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
return this.author.web_url || `mailto:${this.commit.author_email}`;
},
authorAvatar() {
@@ -80,7 +82,7 @@ export default {
v-html="commit.title_html"
></a>
- <span class="commit-row-message d-block d-sm-none"> &middot; {{ commit.short_id }} </span>
+ <span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
<button
v-if="commit.description_html"
diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
index 80aec84f574..1dcdb65d5c7 100644
--- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue
@@ -1,6 +1,6 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
-import { n__, __ } from '~/locale';
+import { n__, __, sprintf } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
@@ -54,11 +54,7 @@ export default {
},
methods: {
commitsText(version) {
- return n__(
- `${version.commits_count} commit,`,
- `${version.commits_count} commits,`,
- version.commits_count,
- );
+ return n__(`%d commit,`, `%d commits,`, version.commits_count);
},
href(version) {
if (this.isBase(version)) {
@@ -76,7 +72,7 @@ export default {
if (this.targetBranch && (this.isBase(version) || !version)) {
return this.targetBranch.branchName;
}
- return `version ${version.version_index}`;
+ return sprintf(__(`version %{versionIndex}`), { versionIndex: version.version_index });
},
isActive(version) {
if (!version) {
@@ -125,9 +121,9 @@ export default {
<div>
<strong>
{{ versionName(version) }}
- <template v-if="isBase(version)">
- (base)
- </template>
+ <template v-if="isBase(version)">{{
+ s__('DiffsCompareBaseBranch|(base)')
+ }}</template>
</strong>
</div>
<div>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index f5876a73eff..63350fafefa 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -151,21 +151,22 @@ export default {
<div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion">
<span class="file-fork-suggestion-note">
- You're not allowed to <span class="js-file-fork-suggestion-section-action">edit</span> files
- in this project directly. Please fork this project, make your changes there, and submit a
- merge request.
+ {{ sprintf(__("You're not allowed to %{tag_start}edit%{tag_end} files in this project
+ directly. Please fork this project, make your changes there, and submit a merge request."),
+ { tag_start: '<span class="js-file-fork-suggestion-section-action">', tag_end: '</span>' })
+ }}
</span>
<a
:href="file.fork_path"
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
- >Fork</a
+ >{{ __('Fork') }}</a
>
<button
class="js-cancel-fork-suggestion-button btn btn-grouped"
type="button"
@click="hideForkMessage"
>
- Cancel
+ {{ __('Cancel') }}
</button>
</div>
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
index 7cf3d90d468..e28909b7be3 100644
--- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
+++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue
@@ -74,7 +74,7 @@ export default {
<button
v-if="discussionsExpanded"
type="button"
- aria-label="Show comments"
+ :aria-label="__('Show comments')"
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
@click="toggleDiscussions"
>
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 891086b4142..f280f3cd26c 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -10,7 +10,7 @@ import { mergeUrlParams } from '../lib/utils/url_utility';
export default class AvailableDropdownMappings {
constructor(
container,
- baseEndpoint,
+ runnerTagsEndpoint,
labelsEndpoint,
milestonesEndpoint,
groupsOnly,
@@ -18,7 +18,7 @@ export default class AvailableDropdownMappings {
includeDescendantGroups,
) {
this.container = container;
- this.baseEndpoint = baseEndpoint;
+ this.runnerTagsEndpoint = runnerTagsEndpoint;
this.labelsEndpoint = labelsEndpoint;
this.milestonesEndpoint = milestonesEndpoint;
this.groupsOnly = groupsOnly;
@@ -149,7 +149,7 @@ export default class AvailableDropdownMappings {
}
getRunnerTagsEndpoint() {
- return `${this.baseEndpoint}/admin/runners/tag_list.json`;
+ return `${this.runnerTagsEndpoint}.json`;
}
getMergeRequestTargetBranchesEndpoint() {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index 1cbfd7f9bb9..835d3bf8a53 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -8,7 +8,7 @@ import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager {
constructor({
- baseEndpoint = '',
+ runnerTagsEndpoint = '',
labelsEndpoint = '',
milestonesEndpoint = '',
tokenizer,
@@ -19,7 +19,7 @@ export default class FilteredSearchDropdownManager {
filteredSearchTokenKeys,
}) {
this.container = FilteredSearchContainer.container;
- this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
+ this.runnerTagsEndpoint = runnerTagsEndpoint.replace(/\/$/, '');
this.labelsEndpoint = labelsEndpoint.replace(/\/$/, '');
this.milestonesEndpoint = milestonesEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer;
@@ -51,7 +51,7 @@ export default class FilteredSearchDropdownManager {
const supportedTokens = this.filteredSearchTokenKeys.getKeys();
const availableMappings = new AvailableDropdownMappings(
this.container,
- this.baseEndpoint,
+ this.runnerTagsEndpoint,
this.labelsEndpoint,
this.milestonesEndpoint,
this.groupsOnly,
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 450e0725f2e..d1f52b91d9e 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -85,7 +85,8 @@ export default class FilteredSearchManager {
if (this.filteredSearchInput) {
this.tokenizer = FilteredSearchTokenizer;
this.dropdownManager = new FilteredSearchDropdownManager({
- baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
+ runnerTagsEndpoint:
+ this.filteredSearchInput.getAttribute('data-runner-tags-endpoint') || '',
labelsEndpoint: this.filteredSearchInput.getAttribute('data-labels-endpoint') || '',
milestonesEndpoint: this.filteredSearchInput.getAttribute('data-milestones-endpoint') || '',
tokenizer: this.tokenizer,
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 3b73dd83c9f..b308cd9c236 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -318,6 +318,7 @@ class GfmAutoComplete {
}
setupLabels($input) {
+ const instance = this;
const fetchData = this.fetchData.bind(this);
const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' };
let command = '';
@@ -348,7 +349,6 @@ class GfmAutoComplete {
}));
},
matcher(flag, subtext) {
- const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
const subtextNodes = subtext
.split(/\n+/g)
.pop()
@@ -366,6 +366,27 @@ class GfmAutoComplete {
return null;
});
+ // If any label matches the inserted text after the last `~`, suggest those labels,
+ // even if any spaces or funky characters were typed.
+ // This allows matching labels like "Accepting merge requests".
+ const labels = instance.cachedData[flag];
+ if (labels) {
+ if (!subtext.includes(flag)) {
+ // Do not match if there is no `~` before the cursor
+ return null;
+ }
+ const lastCandidate = subtext.split(flag).pop();
+ if (labels.find(label => label.title.startsWith(lastCandidate))) {
+ return lastCandidate;
+ }
+ } else {
+ // Load all labels into the autocompleter.
+ // This needs to happen if e.g. editing a label in an existing comment, because normally
+ // label data would only be loaded only once you type `~`.
+ fetchData(this.$inputor, this.at);
+ }
+
+ const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
return match && match.length ? match[1] : null;
},
filter(query, data, searchKey) {
@@ -563,8 +584,9 @@ class GfmAutoComplete {
const accentAChar = decodeURI('%C3%80');
const accentYChar = decodeURI('%C3%BF');
+ // Holy regex, batman!
const regexp = new RegExp(
- `^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`,
+ `^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-:]|[^\\x00-\\x7a])*)$`,
'gi',
);
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 95782b2c88a..1af86a94482 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -30,6 +30,9 @@ export default {
showLoading() {
return !this.currentTree || this.currentTree.loading;
},
+ actualTreeList() {
+ return this.currentTree.tree.filter(entry => !entry.moved);
+ },
},
mounted() {
this.updateViewer(this.viewerType);
@@ -54,9 +57,9 @@ export default {
<slot name="header"></slot>
</header>
<div class="ide-tree-body h-100">
- <template v-if="currentTree.tree.length">
+ <template v-if="actualTreeList.length">
<file-row
- v-for="file in currentTree.tree"
+ v-for="file in actualTreeList"
:key="file.key"
:file="file"
:level="0"
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 5429b834708..507dc363529 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -8,6 +8,7 @@ import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
import service from '../services';
+import router from '../ide_router';
export const redirectToUrl = (self, url) => visitUrl(url);
@@ -208,10 +209,6 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
commit(types.DELETE_ENTRY, path);
- if (entry.parentPath && state.entries[entry.parentPath].tree.length === 0) {
- dispatch('deleteEntry', entry.parentPath);
- }
-
dispatch('triggerFilesChange');
};
@@ -238,10 +235,15 @@ export const renameEntry = (
parentPath: newParentPath,
});
});
- }
+ } else {
+ const newPath = parentPath ? `${parentPath}/${name}` : name;
+ const newEntry = state.entries[newPath];
+ commit(types.TOGGLE_FILE_CHANGED, { file: newEntry, changed: true });
- if (!entryPath && !entry.tempFile) {
- dispatch('deleteEntry', path);
+ if (entry.opened) {
+ router.push(`/project${newEntry.url}`);
+ commit(types.TOGGLE_FILE_OPEN, entry.path);
+ }
}
dispatch('triggerFilesChange');
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index dc40a1fa6a2..7627b6e03af 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -73,7 +73,9 @@ export const getFileData = (
.getFileData(joinPaths(gon.relative_url_root || '', url.replace('/-/', '/')))
.then(({ data, headers }) => {
const normalizedHeaders = normalizeHeaders(headers);
- setPageTitle(decodeURI(normalizedHeaders['PAGE-TITLE']));
+ let title = normalizedHeaders['PAGE-TITLE'];
+ title = file.prevPath ? title.replace(file.prevPath, file.path) : title;
+ setPageTitle(decodeURI(title));
if (data) commit(types.SET_FILE_DATA, { data, file });
if (openFile) commit(types.TOGGLE_FILE_OPEN, path);
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index ae42b87c9a7..ec4c2fdcde2 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -216,15 +216,16 @@ export default {
Vue.set(state.entries, newPath, {
...oldEntry,
id: newPath,
- key: `${newPath}-${oldEntry.type}-${oldEntry.id}`,
+ key: `${newPath}-${oldEntry.type}-${oldEntry.path}`,
path: newPath,
name: entryPath ? oldEntry.name : name,
tempFile: true,
prevPath: oldEntry.tempFile ? null : oldEntry.path,
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
tree: [],
- parentPath,
raw: '',
+ opened: false,
+ parentPath,
});
oldEntry.moved = true;
@@ -241,10 +242,6 @@ export default {
state.changedFiles = state.changedFiles.concat(newEntry);
}
- if (state.entries[newPath].opened) {
- state.openFiles.push(state.entries[newPath]);
- }
-
if (oldEntry.tempFile) {
const filterMethod = f => f.path !== oldEntry.path;
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 4e7a8765abe..fb132c1afc1 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -147,9 +147,9 @@ export const createCommitPayload = ({
commit_message: state.commitMessage || getters.preBuiltCommitMessage,
actions: getCommitFiles(rootState.stagedFiles).map(f => ({
action: commitActionForFile(f),
- file_path: f.path,
+ file_path: f.moved ? f.movedPath : f.path,
previous_path: f.prevPath === '' ? undefined : f.prevPath,
- content: f.content || undefined,
+ content: f.prevPath ? null : f.content || undefined,
encoding: f.base64 ? 'base64' : 'text',
last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha,
})),
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index d895eca7af0..ae579fef25f 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -3,7 +3,7 @@ import { commitItemIconMap } from './constants';
export const getCommitIconMap = file => {
if (file.deleted) {
return commitItemIconMap.deleted;
- } else if (file.tempFile) {
+ } else if (file.tempFile && !file.prevPath) {
return commitItemIconMap.addition;
}
diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue
index 42a3de62772..b2f9296c68b 100644
--- a/app/assets/javascripts/issue_show/components/edit_actions.vue
+++ b/app/assets/javascripts/issue_show/components/edit_actions.vue
@@ -73,7 +73,9 @@ export default {
Save changes
<i v-if="formState.updateLoading" class="fa fa-spinner fa-spin" aria-hidden="true"> </i>
</button>
- <button class="btn btn-default float-right" type="button" @click="closeForm">Cancel</button>
+ <button class="btn btn-default float-right" type="button" @click="closeForm">
+ {{ __('Cancel') }}
+ </button>
<button
v-if="shouldShowDeleteButton"
:class="{ disabled: deleteLoading }"
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index d27dd873125..447d7bf21a5 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -39,7 +39,7 @@ export default {
<template>
<div class="common-note-form">
- <label class="sr-only" for="issue-description"> Description </label>
+ <label class="sr-only" for="issue-description">{{ __('Description') }}</label>
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
@@ -55,8 +55,8 @@ export default {
qa-description-textarea"
dir="auto"
data-supports-quick-actions="false"
- aria-label="Description"
- placeholder="Write a comment or drag your files here…"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable"
>
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue
index 14f0acf6540..6f955928d8e 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -56,22 +56,31 @@ export default {
data-selected="null"
data-toggle="dropdown"
>
- <span class="dropdown-toggle-text"> Choose a template </span>
+ <span class="dropdown-toggle-text">{{ __('Choose a template') }}</span>
<i aria-hidden="true" class="fa fa-chevron-down"> </i>
</button>
<div class="dropdown-menu dropdown-select">
<div class="dropdown-title">
Choose a template
- <button class="dropdown-title-button dropdown-menu-close" aria-label="Close" type="button">
+ <button
+ class="dropdown-title-button dropdown-menu-close"
+ :aria-label="__('Close')"
+ type="button"
+ >
<i aria-hidden="true" class="fa fa-times dropdown-menu-close-icon"> </i>
</button>
</div>
<div class="dropdown-input">
- <input type="search" class="dropdown-input-field" placeholder="Filter" autocomplete="off" />
+ <input
+ type="search"
+ class="dropdown-input-field"
+ :placeholder="__('Filter')"
+ autocomplete="off"
+ />
<i aria-hidden="true" class="fa fa-search dropdown-input-search"> </i>
<i
role="button"
- aria-label="Clear templates search input"
+ :aria-label="__('Clear templates search input')"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
>
</i>
@@ -79,8 +88,12 @@ export default {
<div class="dropdown-content"></div>
<div class="dropdown-footer">
<ul class="dropdown-footer-list">
- <li><a class="no-template"> No template </a></li>
- <li><a class="reset-template"> Reset template </a></li>
+ <li>
+ <a class="no-template">{{ __('No template') }}</a>
+ </li>
+ <li>
+ <a class="reset-template">{{ __('Reset template') }}</a>
+ </li>
</ul>
</div>
</div>
diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue
index ce4baf17d09..34eb0451d53 100644
--- a/app/assets/javascripts/issue_show/components/fields/title.vue
+++ b/app/assets/javascripts/issue_show/components/fields/title.vue
@@ -14,7 +14,7 @@ export default {
<template>
<fieldset>
- <label class="sr-only" for="issuable-title"> Title </label>
+ <label class="sr-only" for="issuable-title">{{ __('Title') }}</label>
<input
id="issuable-title"
ref="input"
@@ -22,8 +22,8 @@ export default {
class="form-control qa-title-input"
dir="auto"
type="text"
- placeholder="Title"
- aria-label="Title"
+ :placeholder="__('Title')"
+ :aria-label="__('Title')"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable"
/>
diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue
index 639221473b1..2f3e611e089 100644
--- a/app/assets/javascripts/issue_show/components/locked_warning.vue
+++ b/app/assets/javascripts/issue_show/components/locked_warning.vue
@@ -10,8 +10,9 @@ export default {
<template>
<div class="alert alert-danger">
- Someone edited the issue at the same time you did. Please check out
- <a :href="currentPath" target="_blank" rel="nofollow">the issue</a> and make sure your changes
- will not unintentionally remove theirs.
+ {{ sprintf(__("Someone edited the issue at the same time you did. Please check out
+ %{linkStart}%the issue%{linkEnd} and make sure your changes will not unintentionally remove
+ theirs."), { linkStart: `<a href="${currentPath}" target="_blank" rel="nofollow">` linkEnd: '</a
+ >', }) }}
</div>
</template>
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index d521c462ad8..062d21ed247 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -479,9 +479,13 @@ export const pikadayToString = date => {
* Seconds can be negative or positive, zero or non-zero. Can be configured for any day
* or week length.
*/
-export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) => {
+export const parseSeconds = (
+ seconds,
+ { daysPerWeek = 5, hoursPerDay = 8, limitToHours = false } = {},
+) => {
const DAYS_PER_WEEK = daysPerWeek;
const HOURS_PER_DAY = hoursPerDay;
+ const SECONDS_PER_MINUTE = 60;
const MINUTES_PER_HOUR = 60;
const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
@@ -493,9 +497,18 @@ export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {})
minutes: 1,
};
- let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
+ if (limitToHours) {
+ timePeriodConstraints.weeks = 0;
+ timePeriodConstraints.days = 0;
+ }
+
+ let unorderedMinutes = Math.abs(seconds / SECONDS_PER_MINUTE);
return _.mapObject(timePeriodConstraints, minutesPerPeriod => {
+ if (minutesPerPeriod === 0) {
+ return 0;
+ }
+
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
unorderedMinutes -= periodCount * minutesPerPeriod;
diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js
new file mode 100644
index 00000000000..e16ddbfef7e
--- /dev/null
+++ b/app/assets/javascripts/manual_ordering.js
@@ -0,0 +1,58 @@
+import Sortable from 'sortablejs';
+import { s__ } from '~/locale';
+import createFlash from '~/flash';
+import {
+ getBoardSortableDefaultOptions,
+ sortableStart,
+} from '~/boards/mixins/sortable_default_options';
+import axios from '~/lib/utils/axios_utils';
+
+const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
+ axios
+ .put(`${url}/reorder`, {
+ move_before_id,
+ move_after_id,
+ group_full_path: issueList.dataset.groupFullPath,
+ })
+ .catch(() => {
+ createFlash(s__("ManualOrdering|Couldn't save the order of the issues"));
+ });
+
+const initManualOrdering = () => {
+ const issueList = document.querySelector('.manual-ordering');
+
+ if (!issueList || !(gon.features && gon.features.manualSorting)) {
+ return;
+ }
+
+ Sortable.create(
+ issueList,
+ getBoardSortableDefaultOptions({
+ scroll: true,
+ dataIdAttr: 'data-id',
+ fallbackOnBody: false,
+ group: {
+ name: 'issues',
+ },
+ draggable: 'li.issue',
+ onStart: () => {
+ sortableStart();
+ },
+ onUpdate: event => {
+ const el = event.item;
+
+ const url = el.getAttribute('url');
+
+ const prev = el.previousElementSibling;
+ const next = el.nextElementSibling;
+
+ const beforeId = prev && parseInt(prev.dataset.id, 10);
+ const afterId = next && parseInt(next.dataset.id, 10);
+
+ updateIssue(url, issueList, { move_after_id: afterId, move_before_id: beforeId });
+ },
+ }),
+ );
+};
+
+export default initManualOrdering;
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 9de4e96e4da..9a3ce5174db 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
@@ -99,7 +100,7 @@ export default {
chartOptions() {
return {
xAxis: {
- name: 'Time',
+ name: __('Time'),
type: 'time',
axisLabel: {
formatter: date => dateFormat(date, 'h:MM TT'),
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 0a652329dfe..23687c54fd3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -106,17 +106,24 @@ export default {
},
customMetricsPath: {
type: String,
- required: true,
+ required: false,
+ default: invalidUrl,
},
validateQueryPath: {
type: String,
- required: true,
+ required: false,
+ default: invalidUrl,
},
dashboardEndpoint: {
type: String,
required: false,
default: invalidUrl,
},
+ currentDashboard: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -139,10 +146,15 @@ export default {
'deploymentData',
'metricsWithData',
'useDashboardEndpoint',
+ 'allDashboards',
+ 'multipleDashboardsEnabled',
]),
groupsWithData() {
return this.groups.filter(group => this.chartsWithData(group.metrics).length > 0);
},
+ selectedDashboardText() {
+ return this.currentDashboard || (this.allDashboards[0] && this.allDashboards[0].display_name);
+ },
},
created() {
this.setEndpoints({
@@ -150,6 +162,7 @@ export default {
environmentsEndpoint: this.environmentsEndpoint,
deploymentsEndpoint: this.deploymentsEndpoint,
dashboardEndpoint: this.dashboardEndpoint,
+ currentDashboard: this.currentDashboard,
});
this.timeWindows = timeWindows;
@@ -234,12 +247,30 @@ export default {
</script>
<template>
- <div v-if="!showEmptyState" class="prometheus-graphs">
+ <div class="prometheus-graphs">
<div class="gl-p-3 border-bottom bg-gray-light d-flex justify-content-between">
<div
v-if="environmentsEndpoint"
class="dropdowns d-flex align-items-center justify-content-between"
>
+ <div v-if="multipleDashboardsEnabled" class="d-flex align-items-center">
+ <label class="mb-0">{{ __('Dashboard') }}</label>
+ <gl-dropdown
+ class="ml-2 mr-3 js-dashboards-dropdown"
+ toggle-class="dropdown-menu-toggle"
+ :text="selectedDashboardText"
+ >
+ <gl-dropdown-item
+ v-for="dashboard in allDashboards"
+ :key="dashboard.path"
+ :active="dashboard.path === currentDashboard"
+ active-class="is-active"
+ :href="`?dashboard=${dashboard.path}`"
+ >
+ {{ dashboard.display_name || dashboard.path }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
<div class="d-flex align-items-center">
<strong>{{ s__('Metrics|Environment') }}</strong>
<gl-dropdown
@@ -253,11 +284,12 @@ export default {
:key="environment.id"
:active="environment.name === currentEnvironmentName"
active-class="is-active"
+ :href="environment.metrics_path"
>{{ environment.name }}</gl-dropdown-item
>
</gl-dropdown>
</div>
- <div class="d-flex align-items-center prepend-left-8">
+ <div v-if="!showEmptyState" class="d-flex align-items-center prepend-left-8">
<strong>{{ s__('Metrics|Show last') }}</strong>
<gl-dropdown
class="prepend-left-10 js-time-window-dropdown"
@@ -276,7 +308,7 @@ export default {
</div>
</div>
<div class="d-flex">
- <div v-if="isEE && canAddMetrics">
+ <div v-if="isEE && canAddMetrics && !showEmptyState">
<gl-button
v-gl-modal-directive="$options.addMetric.modalId"
class="js-add-metric-button text-success border-success"
@@ -317,40 +349,42 @@ export default {
</gl-button>
</div>
</div>
- <graph-group
- v-for="(groupData, index) in groupsWithData"
- :key="index"
- :name="groupData.group"
- :show-panels="showPanels"
- >
- <monitor-area-chart
- v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
- :key="graphIndex"
- :graph-data="graphData"
- :deployment-data="deploymentData"
- :thresholds="getGraphAlertValues(graphData.queries)"
- :container-width="elWidth"
- group-id="monitor-area-chart"
+ <div v-if="!showEmptyState">
+ <graph-group
+ v-for="(groupData, index) in groupsWithData"
+ :key="index"
+ :name="groupData.group"
+ :show-panels="showPanels"
>
- <alert-widget
- v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData"
- :alerts-endpoint="alertsEndpoint"
- :relevant-queries="graphData.queries"
- :alerts-to-manage="getGraphAlerts(graphData.queries)"
- @setAlerts="setAlerts"
- />
- </monitor-area-chart>
- </graph-group>
+ <monitor-area-chart
+ v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
+ :key="graphIndex"
+ :graph-data="graphData"
+ :deployment-data="deploymentData"
+ :thresholds="getGraphAlertValues(graphData.queries)"
+ :container-width="elWidth"
+ group-id="monitor-area-chart"
+ >
+ <alert-widget
+ v-if="isEE && prometheusAlertsAvailable && alertsEndpoint && graphData"
+ :alerts-endpoint="alertsEndpoint"
+ :relevant-queries="graphData.queries"
+ :alerts-to-manage="getGraphAlerts(graphData.queries)"
+ @setAlerts="setAlerts"
+ />
+ </monitor-area-chart>
+ </graph-group>
+ </div>
+ <empty-state
+ v-else
+ :selected-state="emptyState"
+ :documentation-path="documentationPath"
+ :settings-path="settingsPath"
+ :clusters-path="clustersPath"
+ :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
+ :empty-loading-svg-path="emptyLoadingSvgPath"
+ :empty-no-data-svg-path="emptyNoDataSvgPath"
+ :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
+ />
</div>
- <empty-state
- v-else
- :selected-state="emptyState"
- :documentation-path="documentationPath"
- :settings-path="settingsPath"
- :clusters-path="clustersPath"
- :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
- :empty-loading-svg-path="emptyLoadingSvgPath"
- :empty-no-data-svg-path="emptyNoDataSvgPath"
- :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
- />
</template>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index 0e141d02ead..a3c6de14aa4 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -1,4 +1,6 @@
<script>
+import { __ } from '~/locale';
+
export default {
props: {
documentationPath: {
@@ -41,35 +43,35 @@ export default {
states: {
gettingStarted: {
svgUrl: this.emptyGettingStartedSvgPath,
- title: 'Get started with performance monitoring',
- description: `Stay updated about the performance and health
- of your environment by configuring Prometheus to monitor your deployments.`,
- buttonText: 'Install on clusters',
+ title: __('Get started with performance monitoring'),
+ description: __(`Stay updated about the performance and health
+ of your environment by configuring Prometheus to monitor your deployments.`),
+ buttonText: __('Install on clusters'),
buttonPath: this.clustersPath,
- secondaryButtonText: 'Configure existing installation',
+ secondaryButtonText: __('Configure existing installation'),
secondaryButtonPath: this.settingsPath,
},
loading: {
svgUrl: this.emptyLoadingSvgPath,
- title: 'Waiting for performance data',
- description: `Creating graphs uses the data from the Prometheus server.
- If this takes a long time, ensure that data is available.`,
- buttonText: 'View documentation',
+ title: __('Waiting for performance data'),
+ description: __(`Creating graphs uses the data from the Prometheus server.
+ If this takes a long time, ensure that data is available.`),
+ buttonText: __('View documentation'),
buttonPath: this.documentationPath,
},
noData: {
svgUrl: this.emptyNoDataSvgPath,
- title: 'No data found',
- description: `You are connected to the Prometheus server, but there is currently
- no data to display.`,
- buttonText: 'Configure Prometheus',
+ title: __('No data found'),
+ description: __(`You are connected to the Prometheus server, but there is currently
+ no data to display.`),
+ buttonText: __('Configure Prometheus'),
buttonPath: this.settingsPath,
},
unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath,
- title: 'Unable to connect to Prometheus server',
+ title: __('Unable to connect to Prometheus server'),
description: 'Ensure connectivity is available from the GitLab server to the ',
- buttonText: 'View documentation',
+ buttonText: __('View documentation'),
buttonPath: this.documentationPath,
},
},
@@ -90,7 +92,9 @@ export default {
<template>
<div class="row empty-state js-empty-state">
<div class="col-12">
- <div class="state-svg svg-content"><img :src="currentState.svgUrl" /></div>
+ <div class="state-svg svg-content">
+ <img :src="currentState.svgUrl" />
+ </div>
</div>
<div class="col-12">
@@ -98,20 +102,22 @@ export default {
<h4 class="state-title text-center">{{ currentState.title }}</h4>
<p class="state-description">
{{ currentState.description }}
- <a v-if="showButtonDescription" :href="settingsPath"> Prometheus server </a>
+ <a v-if="showButtonDescription" :href="settingsPath">{{ __('Prometheus server') }}</a>
</p>
<div class="text-center">
- <a v-if="currentState.buttonPath" :href="currentState.buttonPath" class="btn btn-success">
- {{ currentState.buttonText }}
- </a>
+ <a
+ v-if="currentState.buttonPath"
+ :href="currentState.buttonPath"
+ class="btn btn-success"
+ >{{ currentState.buttonText }}</a
+ >
<a
v-if="currentState.secondaryButtonPath"
:href="currentState.secondaryButtonPath"
class="btn"
+ >{{ currentState.secondaryButtonText }}</a
>
- {{ currentState.secondaryButtonText }}
- </a>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 1d33537b3b2..edbcf84b342 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterValues } from '~/lib/utils/url_utility';
import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
import store from './stores';
@@ -7,10 +8,12 @@ export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
- store.dispatch(
- 'monitoringDashboard/setDashboardEnabled',
- gon.features.environmentMetricsUsePrometheusEndpoint,
- );
+ store.dispatch('monitoringDashboard/setFeatureFlags', {
+ prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
+ multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards,
+ });
+
+ const [currentDashboard] = getParameterValues('dashboard');
// eslint-disable-next-line no-new
new Vue({
@@ -20,6 +23,7 @@ export default (props = {}) => {
return createElement(Dashboard, {
props: {
...el.dataset,
+ currentDashboard,
hasMetrics: parseBoolean(el.dataset.hasMetrics),
...props,
},
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index f41e215cb5d..0fa2a5d6370 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -35,14 +35,24 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints);
};
-export const setDashboardEnabled = ({ commit }, enabled) => {
- commit(types.SET_DASHBOARD_ENABLED, enabled);
+export const setFeatureFlags = (
+ { commit },
+ { prometheusEndpointEnabled, multipleDashboardsEnabled },
+) => {
+ commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled);
+ commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled);
};
export const requestMetricsDashboard = ({ commit }) => {
commit(types.REQUEST_METRICS_DATA);
};
-export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => {
+export const receiveMetricsDashboardSuccess = (
+ { state, commit, dispatch },
+ { response, params },
+) => {
+ if (state.multipleDashboardsEnabled) {
+ commit(types.SET_ALL_DASHBOARDS, response.all_dashboards);
+ }
commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups);
dispatch('fetchPrometheusMetrics', params);
};
@@ -95,6 +105,11 @@ export const fetchMetricsData = ({ state, dispatch }, params) => {
export const fetchDashboard = ({ state, dispatch }, params) => {
dispatch('requestMetricsDashboard');
+ if (state.currentDashboard) {
+ // eslint-disable-next-line no-param-reassign
+ params.dashboard = state.currentDashboard;
+ }
+
return axios
.get(state.dashboardEndpoint, { params })
.then(resp => resp.data)
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 63894e83362..2c78a0b9315 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -10,6 +10,8 @@ export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAIL
export const SET_QUERY_RESULT = 'SET_QUERY_RESULT';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_DASHBOARD_ENABLED = 'SET_DASHBOARD_ENABLED';
+export const SET_MULTIPLE_DASHBOARDS_ENABLED = 'SET_MULTIPLE_DASHBOARDS_ENABLED';
+export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index d4b816e2717..a85a7723c1f 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -74,10 +74,14 @@ export default {
state.environmentsEndpoint = endpoints.environmentsEndpoint;
state.deploymentsEndpoint = endpoints.deploymentsEndpoint;
state.dashboardEndpoint = endpoints.dashboardEndpoint;
+ state.currentDashboard = endpoints.currentDashboard;
},
[types.SET_DASHBOARD_ENABLED](state, enabled) {
state.useDashboardEndpoint = enabled;
},
+ [types.SET_MULTIPLE_DASHBOARDS_ENABLED](state, enabled) {
+ state.multipleDashboardsEnabled = enabled;
+ },
[types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted';
},
@@ -85,4 +89,7 @@ export default {
state.showEmptyState = true;
state.emptyState = 'noData';
},
+ [types.SET_ALL_DASHBOARDS](state, dashboards) {
+ state.allDashboards = dashboards;
+ },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index c33529cd588..de711d6ccae 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -8,10 +8,13 @@ export default () => ({
deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl,
useDashboardEndpoint: false,
+ multipleDashboardsEnabled: false,
emptyState: 'gettingStarted',
showEmptyState: true,
groups: [],
deploymentData: [],
environments: [],
metricsWithData: [],
+ allDashboards: [],
+ currentDashboard: null,
});
diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js
index 9055738f86e..2ffeed8a584 100644
--- a/app/assets/javascripts/pages/dashboard/issues/index.js
+++ b/app/assets/javascripts/pages/dashboard/issues/index.js
@@ -2,6 +2,7 @@ import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/pages/constants';
+import initManualOrdering from '~/manual_ordering';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
@@ -10,4 +11,5 @@ document.addEventListener('DOMContentLoaded', () => {
});
projectSelect();
+ initManualOrdering();
});
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 35d4b034654..23fb5656008 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -2,6 +2,7 @@ import projectSelect from '~/project_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import initManualOrdering from '~/manual_ordering';
document.addEventListener('DOMContentLoaded', () => {
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
@@ -12,4 +13,5 @@ document.addEventListener('DOMContentLoaded', () => {
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
projectSelect();
+ initManualOrdering();
});
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index c34aff02111..c73ebb31eb3 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -7,6 +7,7 @@ import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
+import initManualOrdering from '~/manual_ordering';
document.addEventListener('DOMContentLoaded', () => {
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
new ShortcutsNavigation();
new UsersSelect();
+ initManualOrdering();
});
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index d67d88c4dba..c8819cf35cf 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -1,8 +1,8 @@
import Visibility from 'visibilityjs';
+import PipelineStore from 'ee_else_ce/pipelines/stores/pipeline_store';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
import { __ } from '../locale';
-import PipelineStore from './stores/pipeline_store';
import PipelineService from './services/pipeline_service';
export default class pipelinesMediator {
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index 0d4d431855c..67963dc1923 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -36,7 +36,7 @@ export default {
to: `/tree/${this.ref}${path}`,
});
},
- [{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}` }],
+ [{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}/` }],
);
},
},
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 891e3fe9d16..1e66ccbfa29 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -131,7 +131,9 @@ export default {
v-for="entry in val"
:id="entry.id"
:key="`${entry.flatPath}-${entry.id}`"
+ :project-path="projectPath"
:current-path="path"
+ :name="entry.name"
:path="entry.flatPath"
:type="entry.type"
:url="entry.webUrl"
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 4519f82fc93..c31e7fa71a2 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,12 +1,30 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlLink, GlSkeletonLoading } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { getIconName } from '../../utils/icon';
import getRefMixin from '../../mixins/get_ref';
+import getCommit from '../../queries/getCommit.query.graphql';
export default {
components: {
GlBadge,
+ GlLink,
+ GlSkeletonLoading,
+ TimeagoTooltip,
+ },
+ apollo: {
+ commit: {
+ query: getCommit,
+ variables() {
+ return {
+ fileName: this.name,
+ type: this.type,
+ path: this.currentPath,
+ projectPath: this.projectPath,
+ };
+ },
+ },
},
mixins: [getRefMixin],
props: {
@@ -14,10 +32,18 @@ export default {
type: String,
required: true,
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
currentPath: {
type: String,
required: true,
},
+ name: {
+ type: String,
+ required: true,
+ },
path: {
type: String,
required: true,
@@ -37,6 +63,11 @@ export default {
default: null,
},
},
+ data() {
+ return {
+ commit: null,
+ };
+ },
computed: {
routerLinkTo() {
return this.isFolder ? { path: `/tree/${this.ref}/${this.path}` } : null;
@@ -73,7 +104,7 @@ export default {
</script>
<template>
- <tr v-once :class="`file_${id}`" class="tree-item" @click="openRow">
+ <tr :class="`file_${id}`" class="tree-item" @click="openRow">
<td class="tree-item-file-name">
<i :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
<component :is="linkComponent" :to="routerLinkTo" :href="url" class="str-truncated">
@@ -83,10 +114,18 @@ export default {
LFS
</gl-badge>
<template v-if="isSubmodule">
- @ <a href="#" class="commit-sha">{{ shortSha }}</a>
+ @ <gl-link href="#" class="commit-sha">{{ shortSha }}</gl-link>
</template>
</td>
- <td class="d-none d-sm-table-cell tree-commit"></td>
- <td class="tree-time-ago text-right"></td>
+ <td class="d-none d-sm-table-cell tree-commit">
+ <gl-link v-if="commit" :href="commit.commitPath" class="str-truncated-100 tree-commit-link">
+ {{ commit.message }}
+ </gl-link>
+ <gl-skeleton-loading v-else :lines="1" class="h-auto" />
+ </td>
+ <td class="tree-time-ago text-right">
+ <timeago-tooltip v-if="commit" :time="commit.committedDate" tooltip-placement="bottom" />
+ <gl-skeleton-loading v-else :lines="1" class="ml-auto h-auto w-50" />
+ </td>
</tr>
</template>
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index ef147ec15cb..6cb253c8169 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './fragmentTypes.json';
+import { fetchLogsTree } from './log_tree';
Vue.use(VueApollo);
@@ -13,7 +14,21 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
});
const defaultClient = createDefaultClient(
- {},
+ {
+ Query: {
+ commit(_, { path, fileName, type }) {
+ return new Promise(resolve => {
+ fetchLogsTree(defaultClient, path, '0', {
+ resolve,
+ entry: {
+ name: fileName,
+ type,
+ },
+ });
+ });
+ },
+ },
+ },
{
cacheConfig: {
fragmentMatcher,
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index d9216e88676..6280977b05b 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -16,6 +16,7 @@ export default function setupVueRepositoryList() {
projectPath,
projectShortPath,
ref,
+ commits: [],
},
});
diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js
new file mode 100644
index 00000000000..2c19aca2397
--- /dev/null
+++ b/app/assets/javascripts/repository/log_tree.js
@@ -0,0 +1,64 @@
+import axios from '~/lib/utils/axios_utils';
+import getCommits from './queries/getCommits.query.graphql';
+import getProjectPath from './queries/getProjectPath.query.graphql';
+import getRef from './queries/getRef.query.graphql';
+
+let fetchpromise;
+let resolvers = [];
+
+export function normalizeData(data) {
+ return data.map(d => ({
+ sha: d.commit.id,
+ message: d.commit.message,
+ committedDate: d.commit.committed_date,
+ commitPath: d.commit_path,
+ fileName: d.file_name,
+ type: d.type,
+ __typename: 'LogTreeCommit',
+ }));
+}
+
+export function resolveCommit(commits, { resolve, entry }) {
+ const commit = commits.find(c => c.fileName === entry.name && c.type === entry.type);
+
+ if (commit) {
+ resolve(commit);
+ }
+}
+
+export function fetchLogsTree(client, path, offset, resolver = null) {
+ if (resolver) {
+ resolvers.push(resolver);
+ }
+
+ if (fetchpromise) return fetchpromise;
+
+ const { projectPath } = client.readQuery({ query: getProjectPath });
+ const { ref } = client.readQuery({ query: getRef });
+
+ fetchpromise = axios
+ .get(`${gon.gitlab_url}/${projectPath}/refs/${ref}/logs_tree${path ? `/${path}` : ''}`, {
+ params: { format: 'json', offset },
+ })
+ .then(({ data, headers }) => {
+ const headerLogsOffset = headers['more-logs-offset'];
+ const { commits } = client.readQuery({ query: getCommits });
+ const newCommitData = [...commits, ...normalizeData(data)];
+ client.writeQuery({
+ query: getCommits,
+ data: { commits: newCommitData },
+ });
+
+ resolvers.forEach(r => resolveCommit(newCommitData, r));
+
+ fetchpromise = null;
+
+ if (headerLogsOffset) {
+ fetchLogsTree(client, path, headerLogsOffset);
+ } else {
+ resolvers = [];
+ }
+ });
+
+ return fetchpromise;
+}
diff --git a/app/assets/javascripts/repository/queries/getCommit.query.graphql b/app/assets/javascripts/repository/queries/getCommit.query.graphql
new file mode 100644
index 00000000000..e2a2d831e47
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getCommit.query.graphql
@@ -0,0 +1,10 @@
+query getCommit($fileName: String!, $type: String!, $path: String!) {
+ commit(path: $path, fileName: $fileName, type: $type) @client {
+ sha
+ message
+ committedDate
+ commitPath
+ fileName
+ type
+ }
+}
diff --git a/app/assets/javascripts/repository/queries/getCommits.query.graphql b/app/assets/javascripts/repository/queries/getCommits.query.graphql
new file mode 100644
index 00000000000..df9e67cc440
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getCommits.query.graphql
@@ -0,0 +1,10 @@
+query getCommits {
+ commits @client {
+ sha
+ message
+ committedDate
+ commitPath
+ fileName
+ type
+ }
+}
diff --git a/app/assets/javascripts/repository/queries/getFiles.query.graphql b/app/assets/javascripts/repository/queries/getFiles.query.graphql
index ef924fde556..4c24fc4087f 100644
--- a/app/assets/javascripts/repository/queries/getFiles.query.graphql
+++ b/app/assets/javascripts/repository/queries/getFiles.query.graphql
@@ -1,5 +1,6 @@
fragment TreeEntry on Entry {
id
+ name
flatPath
type
}
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index f4d926cd3ec..bc263bc36e4 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -28,11 +28,16 @@ export default {
type: String,
required: true,
},
+ limitToHours: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
parsedTimeRemaining() {
const diffSeconds = this.timeEstimate - this.timeSpent;
- return parseSeconds(diffSeconds);
+ return parseSeconds(diffSeconds, { limitToHours: this.limitToHours });
},
timeRemainingHumanReadable() {
return stringifyTime(this.parsedTimeRemaining);
@@ -65,9 +70,6 @@ export default {
:title="timeRemainingTooltip"
:class="timeRemainingStatusClass"
class="compare-meter"
- data-toggle="tooltip"
- data-placement="top"
- role="timeRemainingDisplay"
>
<gl-progress-bar :value="timeRemainingPercent" :variant="progressBarVariant" />
<div class="compare-display-container">
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 8e8b9f19b6e..018b30d2a67 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -53,6 +53,7 @@ export default {
:time-spent="store.totalTimeSpent"
:human-time-estimate="store.humanTimeEstimate"
:human-time-spent="store.humanTotalTimeSpent"
+ :limit-to-hours="store.timeTrackingLimitToHours"
:root-path="store.rootPath"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index d84d5344935..682ca600b6a 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -37,6 +37,10 @@ export default {
required: false,
default: '',
},
+ limitToHours: {
+ type: Boolean,
+ default: false,
+ },
rootPath: {
type: String,
required: true,
@@ -129,6 +133,7 @@ export default {
:time-spent="timeSpent"
:time-spent-human-readable="humanTimeSpent"
:time-estimate-human-readable="humanTimeEstimate"
+ :limit-to-hours="limitToHours"
/>
<transition name="help-state-toggle">
<time-tracking-help-state v-if="showHelpState" :root-path="rootPath" />
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index 1ebdbec7bc9..d934463382f 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import timeTracker from './components/time_tracking/time_tracker.vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
export default class SidebarMilestone {
constructor() {
@@ -7,7 +8,7 @@ export default class SidebarMilestone {
if (!el) return;
- const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = el.dataset;
+ const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent, limitToHours } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -22,6 +23,7 @@ export default class SidebarMilestone {
timeSpent: parseInt(timeSpent, 10),
humanTimeEstimate,
humanTimeSpent,
+ limitToHours: parseBoolean(limitToHours),
rootPath: '/',
},
}),
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 22ac8df9699..643fe6c00b6 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,7 +1,7 @@
import { visitUrl } from '../lib/utils/url_utility';
import Flash from '../flash';
import Service from './services/sidebar_service';
-import Store from './stores/sidebar_store';
+import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
import { __ } from '~/locale';
export default class SidebarMediator {
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 7b8b4c5d856..63c4a2a3f84 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -8,7 +8,7 @@ export default class SidebarStore {
}
initSingleton(options) {
- const { currentUser, rootPath, editable } = options;
+ const { currentUser, rootPath, editable, timeTrackingLimitToHours } = options;
this.currentUser = currentUser;
this.rootPath = rootPath;
this.editable = editable;
@@ -16,6 +16,7 @@ export default class SidebarStore {
this.totalTimeSpent = 0;
this.humanTimeEstimate = '';
this.humanTimeSpent = '';
+ this.timeTrackingLimitToHours = timeTrackingLimitToHours;
this.assignees = [];
this.isFetching = {
assignees: true,
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment.js b/app/assets/javascripts/visual_review_toolbar/components/comment.js
index 2fec96d1435..04bfb5e9532 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/comment.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/comment.js
@@ -1,54 +1,62 @@
import { BLACK, COMMENT_BOX, MUTED, LOGOUT } from './constants';
-import { clearNote, note, postError } from './note';
-import { buttonClearStyles, selectCommentBox, selectCommentButton, selectNote } from './utils';
+import { clearNote, postError } from './note';
+import {
+ buttonClearStyles,
+ selectCommentBox,
+ selectCommentButton,
+ selectNote,
+ selectNoteContainer,
+} from './utils';
const comment = `
<div>
<textarea id="${COMMENT_BOX}" name="${COMMENT_BOX}" rows="3" placeholder="Enter your feedback or idea" class="gitlab-input" aria-required="true"></textarea>
- ${note}
<p class="gitlab-metadata-note">Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p>
</div>
<div class="gitlab-button-wrapper">
- <button class="gitlab-button gitlab-button-secondary" style="${buttonClearStyles}" type="button" id="${LOGOUT}"> Logout </button>
+ <button class="gitlab-button gitlab-button-secondary" style="${buttonClearStyles}" type="button" id="${LOGOUT}"> Log out </button>
<button class="gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="gitlab-comment-button"> Send feedback </button>
</div>
`;
-const resetCommentBox = () => {
- const commentBox = selectCommentBox();
+const resetCommentButton = () => {
const commentButton = selectCommentButton();
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
commentButton.innerText = 'Send feedback';
commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success');
commentButton.style.opacity = 1;
+};
+const resetCommentBox = () => {
+ const commentBox = selectCommentBox();
commentBox.style.pointerEvents = 'auto';
commentBox.style.color = BLACK;
};
-const resetCommentButton = () => {
+const resetCommentText = () => {
const commentBox = selectCommentBox();
- const currentNote = selectNote();
-
commentBox.value = '';
- currentNote.innerText = '';
};
const resetComment = () => {
- resetCommentBox();
resetCommentButton();
+ resetCommentBox();
+ resetCommentText();
};
-const confirmAndClear = mergeRequestId => {
+const confirmAndClear = feedbackInfo => {
const commentButton = selectCommentButton();
const currentNote = selectNote();
+ const noteContainer = selectNoteContainer();
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
commentButton.innerText = 'Feedback sent';
- /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
- currentNote.innerText = `Your comment was successfully posted to merge request #${mergeRequestId}`;
- setTimeout(resetComment, 2000);
+ noteContainer.style.visibility = 'visible';
+ currentNote.insertAdjacentHTML('beforeend', feedbackInfo);
+
+ setTimeout(resetComment, 1000);
+ setTimeout(clearNote, 6000);
};
const setInProgressState = () => {
@@ -71,6 +79,7 @@ const postComment = ({
innerWidth,
innerHeight,
projectId,
+ projectPath,
mergeRequestId,
mrUrl,
token,
@@ -86,6 +95,7 @@ const postComment = ({
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
postError('Your comment appears to be empty.', COMMENT_BOX);
resetCommentBox();
+ resetCommentButton();
return;
}
@@ -114,18 +124,24 @@ const postComment = ({
})
.then(response => {
if (response.ok) {
- confirmAndClear(mergeRequestId);
- return;
+ return response.json();
}
throw new Error(`${response.status}: ${response.statusText}`);
})
+ .then(data => {
+ const commentId = data.notes[0].id;
+ const feedbackLink = `${mrUrl}/${projectPath}/merge_requests/${mergeRequestId}#note_${commentId}`;
+ const feedbackInfo = `Feedback sent. View at <a class="gitlab-link" href="${feedbackLink}">${projectPath} #${mergeRequestId} (comment ${commentId})</a>`;
+ confirmAndClear(feedbackInfo);
+ })
.catch(err => {
postError(
`Your comment could not be sent. Please try again. Error: ${err.message}`,
COMMENT_BOX,
);
resetCommentBox();
+ resetCommentButton();
});
};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/constants.js b/app/assets/javascripts/visual_review_toolbar/components/constants.js
index 32ed1153515..07fcb179d15 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/constants.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/constants.js
@@ -2,10 +2,12 @@
const COLLAPSE_BUTTON = 'gitlab-collapse';
const COMMENT_BOX = 'gitlab-comment';
const COMMENT_BUTTON = 'gitlab-comment-button';
-const FORM = 'gitlab-form-wrapper';
+const FORM = 'gitlab-form';
+const FORM_CONTAINER = 'gitlab-form-wrapper';
const LOGIN = 'gitlab-login';
const LOGOUT = 'gitlab-logout-button';
const NOTE = 'gitlab-validation-note';
+const NOTE_CONTAINER = 'gitlab-note-wrapper';
const REMEMBER_TOKEN = 'gitlab-remember_token';
const REVIEW_CONTAINER = 'gitlab-review-container';
const TOKEN_BOX = 'gitlab-token';
@@ -16,16 +18,18 @@ const BLACK = 'rgba(46, 46, 46, 1)';
const CLEAR = 'rgba(255, 255, 255, 0)';
const MUTED = 'rgba(223, 223, 223, 0.5)';
const RED = 'rgba(219, 59, 33, 1)';
-const WHITE = 'rgba(255, 255, 255, 1)';
+const WHITE = 'rgba(250, 250, 250, 1)';
export {
COLLAPSE_BUTTON,
COMMENT_BOX,
COMMENT_BUTTON,
FORM,
+ FORM_CONTAINER,
LOGIN,
LOGOUT,
NOTE,
+ NOTE_CONTAINER,
REMEMBER_TOKEN,
REVIEW_CONTAINER,
TOKEN_BOX,
diff --git a/app/assets/javascripts/visual_review_toolbar/components/index.js b/app/assets/javascripts/visual_review_toolbar/components/index.js
index 43581818152..50b52d7d3a2 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/index.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/index.js
@@ -1,22 +1,32 @@
import { comment, postComment } from './comment';
-import { COLLAPSE_BUTTON, COMMENT_BUTTON, LOGIN, LOGOUT, REVIEW_CONTAINER } from './constants';
+import {
+ COLLAPSE_BUTTON,
+ COMMENT_BUTTON,
+ FORM_CONTAINER,
+ LOGIN,
+ LOGOUT,
+ REVIEW_CONTAINER,
+} from './constants';
import { authorizeUser, login } from './login';
+import { note } from './note';
import { selectContainer } from './utils';
-import { form, logoutUser, toggleForm } from './wrapper';
+import { buttonAndForm, logoutUser, toggleForm } from './wrapper';
import { collapseButton } from './wrapper_icons';
export {
authorizeUser,
+ buttonAndForm,
collapseButton,
comment,
- form,
login,
logoutUser,
+ note,
postComment,
selectContainer,
toggleForm,
COLLAPSE_BUTTON,
COMMENT_BUTTON,
+ FORM_CONTAINER,
LOGIN,
LOGOUT,
REVIEW_CONTAINER,
diff --git a/app/assets/javascripts/visual_review_toolbar/components/login.js b/app/assets/javascripts/visual_review_toolbar/components/login.js
index ce713cdc520..0a71299f041 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/login.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/login.js
@@ -1,5 +1,5 @@
import { LOGIN, REMEMBER_TOKEN, TOKEN_BOX } from './constants';
-import { clearNote, note, postError } from './note';
+import { clearNote, postError } from './note';
import { buttonClearStyles, selectRemember, selectToken } from './utils';
import { addCommentForm } from './wrapper';
@@ -7,7 +7,6 @@ const login = `
<div>
<label for="${TOKEN_BOX}" class="gitlab-label">Enter your <a class="gitlab-link" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a></label>
<input class="gitlab-input" type="password" id="${TOKEN_BOX}" name="${TOKEN_BOX}" aria-required="true" autocomplete="current-password">
- ${note}
</div>
<div class="gitlab-checkbox-wrapper">
<input type="checkbox" id="${REMEMBER_TOKEN}" name="${REMEMBER_TOKEN}" value="remember">
diff --git a/app/assets/javascripts/visual_review_toolbar/components/note.js b/app/assets/javascripts/visual_review_toolbar/components/note.js
index dfebf58fd95..0150f640aae 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/note.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/note.js
@@ -1,14 +1,19 @@
-import { NOTE, RED } from './constants';
-import { selectById, selectNote } from './utils';
+import { NOTE, NOTE_CONTAINER, RED } from './constants';
+import { selectById, selectNote, selectNoteContainer } from './utils';
const note = `
- <p id=${NOTE} class='gitlab-message'></p>
+ <div id="${NOTE_CONTAINER}" style="visibility: hidden;">
+ <p id="${NOTE}" class="gitlab-message"></p>
+ </div>
`;
const clearNote = inputId => {
const currentNote = selectNote();
+ const noteContainer = selectNoteContainer();
+
currentNote.innerText = '';
currentNote.style.color = '';
+ noteContainer.style.visibility = 'hidden';
if (inputId) {
const field = document.getElementById(inputId);
@@ -18,10 +23,13 @@ const clearNote = inputId => {
const postError = (message, inputId) => {
const currentNote = selectNote();
+ const noteContainer = selectNoteContainer();
const field = selectById(inputId);
field.style.borderColor = RED;
currentNote.style.color = RED;
currentNote.innerText = message;
+ noteContainer.style.visibility = 'visible';
+ setTimeout(clearNote.bind(null, inputId), 5000);
};
export { clearNote, note, postError };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/utils.js b/app/assets/javascripts/visual_review_toolbar/components/utils.js
index 7bc2e5a905b..00f4460925d 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/utils.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/utils.js
@@ -5,7 +5,9 @@ import {
COMMENT_BOX,
COMMENT_BUTTON,
FORM,
+ FORM_CONTAINER,
NOTE,
+ NOTE_CONTAINER,
REMEMBER_TOKEN,
REVIEW_CONTAINER,
TOKEN_BOX,
@@ -24,7 +26,9 @@ const selectCommentBox = () => document.getElementById(COMMENT_BOX);
const selectCommentButton = () => document.getElementById(COMMENT_BUTTON);
const selectContainer = () => document.getElementById(REVIEW_CONTAINER);
const selectForm = () => document.getElementById(FORM);
+const selectFormContainer = () => document.getElementById(FORM_CONTAINER);
const selectNote = () => document.getElementById(NOTE);
+const selectNoteContainer = () => document.getElementById(NOTE_CONTAINER);
const selectRemember = () => document.getElementById(REMEMBER_TOKEN);
const selectToken = () => document.getElementById(TOKEN_BOX);
@@ -36,7 +40,9 @@ export {
selectCommentBox,
selectCommentButton,
selectForm,
+ selectFormContainer,
selectNote,
+ selectNoteContainer,
selectRemember,
selectToken,
};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
index 233b7ec496c..f2eaf1d7916 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
@@ -1,15 +1,28 @@
import { comment } from './comment';
-import { CLEAR, FORM, WHITE } from './constants';
+import { CLEAR, FORM, FORM_CONTAINER, WHITE } from './constants';
import { login } from './login';
-import { selectCollapseButton, selectContainer, selectForm } from './utils';
+import { clearNote } from './note';
+import {
+ selectCollapseButton,
+ selectForm,
+ selectFormContainer,
+ selectNoteContainer,
+} from './utils';
import { commentIcon, compressIcon } from './wrapper_icons';
const form = content => `
- <form id=${FORM}>
+ <form id="${FORM}">
${content}
</form>
`;
+const buttonAndForm = ({ content, toggleButton }) => `
+ <div id="${FORM_CONTAINER}" class="gitlab-form-open">
+ ${toggleButton}
+ ${form(content)}
+ </div>
+`;
+
const addCommentForm = () => {
const formWrapper = selectForm();
formWrapper.innerHTML = comment;
@@ -31,13 +44,15 @@ function logoutUser() {
return;
}
+ clearNote();
addLoginForm();
}
function toggleForm() {
- const container = selectContainer();
const collapseButton = selectCollapseButton();
const currentForm = selectForm();
+ const formContainer = selectFormContainer();
+ const noteContainer = selectNoteContainer();
const OPEN = 'open';
const CLOSED = 'closed';
@@ -49,7 +64,7 @@ function toggleForm() {
const openButtonClasses = ['gitlab-collapse-closed', 'gitlab-collapse-open'];
const closedButtonClasses = [...openButtonClasses].reverse();
- const openContainerClasses = ['gitlab-closed-wrapper', 'gitlab-open-wrapper'];
+ const openContainerClasses = ['gitlab-wrapper-closed', 'gitlab-wrapper-open'];
const closedContainerClasses = [...openContainerClasses].reverse();
const stateVals = {
@@ -72,11 +87,16 @@ function toggleForm() {
const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
const currentVals = stateVals[nextState];
- container.classList.replace(...currentVals.containerClasses);
- container.style.backgroundColor = currentVals.backgroundColor;
+ formContainer.classList.replace(...currentVals.containerClasses);
+ formContainer.style.backgroundColor = currentVals.backgroundColor;
+ formContainer.classList.toggle('gitlab-form-open');
currentForm.style.display = currentVals.display;
collapseButton.classList.replace(...currentVals.buttonClasses);
collapseButton.innerHTML = currentVals.icon;
+
+ if (noteContainer && noteContainer.innerText.length > 0) {
+ noteContainer.style.display = currentVals.display;
+ }
}
-export { addCommentForm, addLoginForm, form, logoutUser, toggleForm };
+export { addCommentForm, addLoginForm, buttonAndForm, logoutUser, toggleForm };
diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js
index 941d77e25b4..f94eb88835a 100644
--- a/app/assets/javascripts/visual_review_toolbar/index.js
+++ b/app/assets/javascripts/visual_review_toolbar/index.js
@@ -1,6 +1,6 @@
import './styles/toolbar.css';
-import { form, selectContainer, REVIEW_CONTAINER } from './components';
+import { buttonAndForm, note, selectContainer, REVIEW_CONTAINER } from './components';
import { debounce, eventLookup, getInitialView, initializeState, updateWindowSize } from './store';
/*
@@ -20,12 +20,11 @@ import { debounce, eventLookup, getInitialView, initializeState, updateWindowSiz
window.addEventListener('load', () => {
initializeState(window, document);
- const { content, toggleButton } = getInitialView(window);
+ const mainContent = buttonAndForm(getInitialView(window));
const container = document.createElement('div');
-
container.setAttribute('id', REVIEW_CONTAINER);
- container.insertAdjacentHTML('beforeend', toggleButton);
- container.insertAdjacentHTML('beforeend', form(content));
+ container.insertAdjacentHTML('beforeend', note);
+ container.insertAdjacentHTML('beforeend', mainContent);
document.body.insertBefore(container, document.body.firstChild);
diff --git a/app/assets/javascripts/visual_review_toolbar/store/state.js b/app/assets/javascripts/visual_review_toolbar/store/state.js
index f5ede6e85b2..22702d524b8 100644
--- a/app/assets/javascripts/visual_review_toolbar/store/state.js
+++ b/app/assets/javascripts/visual_review_toolbar/store/state.js
@@ -34,7 +34,7 @@ const initializeState = (wind, doc) => {
const browser = getBrowserId(userAgent);
const scriptEl = doc.getElementById('review-app-toolbar-script');
- const { projectId, mergeRequestId, mrUrl } = scriptEl.dataset;
+ const { projectId, mergeRequestId, mrUrl, projectPath } = scriptEl.dataset;
// This mutates our default state object above. It's weird but it makes the linter happy.
Object.assign(state, {
@@ -46,6 +46,7 @@ const initializeState = (wind, doc) => {
mrUrl,
platform,
projectId,
+ projectPath,
userAgent,
});
};
diff --git a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
index 342b3599a44..00a55c0027a 100644
--- a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
+++ b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
@@ -6,23 +6,42 @@
pointer-events: none;
}
-#gitlab-form-wrapper {
+#gitlab-comment {
+ background-color: #fafafa;
+}
+
+#gitlab-form {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ margin-bottom: 0;
+}
+
+#gitlab-note-wrapper {
display: flex;
flex-direction: column;
- width: 100%
+ background-color: #fafafa;
+ border-radius: 4px;
+ margin-bottom: .5rem;
+ padding: 1rem;
+}
+
+#gitlab-form-wrapper {
+ overflow: auto;
+ display: flex;
+ flex-direction: row-reverse;
+ border-radius: 4px;
}
#gitlab-review-container {
max-width: 22rem;
max-height: 22rem;
- overflow: scroll;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
position: fixed;
bottom: 1rem;
right: 1rem;
- display: flex;
- flex-direction: row-reverse;
- padding: 1rem;
- background-color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
@@ -31,12 +50,12 @@
color: #2e2e2e;
}
-.gitlab-open-wrapper {
+.gitlab-wrapper-open {
max-width: 22rem;
max-height: 22rem;
}
-.gitlab-closed-wrapper {
+.gitlab-wrapper-closed {
max-width: 3.4rem;
max-height: 3.4rem;
}
@@ -47,7 +66,7 @@
}
.gitlab-button-secondary {
- background: none #fff;
+ background: none #fafafa;
margin: 0 .5rem;
border: 1px solid #e3e3e3;
}
@@ -113,6 +132,11 @@
align-items: baseline;
}
+.gitlab-form-open {
+ padding: 1rem;
+ background-color: #fafafa;
+}
+
.gitlab-label {
font-weight: 600;
display: inline-block;
@@ -126,6 +150,10 @@
background-image: none;
}
+.gitlab-link:hover {
+ text-decoration: underline;
+}
+
.gitlab-message {
padding: .25rem 0;
margin: 0;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index dcbb23684d1..6a0127eb51c 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -594,18 +594,18 @@
padding: 16px 0;
small {
- color: $gray-darkest;
+ color: $gray-700;
}
}
.edited-text {
- color: $gray-darkest;
+ color: $gray-700;
display: block;
margin: 16px 0 0;
font-size: 85%;
.author-link {
- color: $gray-darkest;
+ color: $gray-700;
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 48289c8f381..8359a60ec9f 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -1,4 +1,18 @@
.issues-list {
+ &.manual-ordering {
+ background-color: $gray-light;
+ border-radius: $border-radius-default;
+ padding: $gl-padding-8;
+
+ .issue {
+ background-color: $white-light;
+ margin-bottom: $gl-padding-8;
+ border-radius: $border-radius-default;
+ border: 1px solid $gray-100;
+ box-shadow: 0 1px 2px $issue-boards-card-shadow;
+ }
+ }
+
.issue {
padding: 10px 0 10px $gl-padding;
position: relative;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 5cacd42bf0d..824edb2869f 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -628,7 +628,7 @@ $note-form-margin-left: 72px;
.note-headline-meta {
.system-note-separator {
- color: $gl-text-color-disabled;
+ color: $gray-700;
}
.note-timestamp {
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
index 54c0510497f..d5830f6648c 100644
--- a/app/controllers/concerns/continue_params.rb
+++ b/app/controllers/concerns/continue_params.rb
@@ -6,7 +6,7 @@ module ContinueParams
def continue_params
continue_params = params[:continue]
- return unless continue_params
+ return {} unless continue_params
continue_params = continue_params.permit(:to, :notice, :notice_now)
continue_params[:to] = safe_redirect_path(continue_params[:to])
diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb
index 6785e6972d0..fa3716502a0 100644
--- a/app/controllers/concerns/internal_redirect.rb
+++ b/app/controllers/concerns/internal_redirect.rb
@@ -5,8 +5,8 @@ module InternalRedirect
def safe_redirect_path(path)
return unless path
- # Verify that the string starts with a `/` but not a double `/`.
- return unless path =~ %r{^/\w.*$}
+ # Verify that the string starts with a `/` and a known route character.
+ return unless path =~ %r{^/[-\w].*$}
uri = URI(path)
# Ignore anything path of the redirect except for the path, querystring and,
diff --git a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
index 426f224d26b..f47ead2f0da 100644
--- a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
+++ b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
@@ -14,6 +14,10 @@ module RequiresWhitelistedMonitoringClient
end
def client_ip_whitelisted?
+ # Always allow developers to access http://localhost:3000/-/metrics for
+ # debugging purposes
+ return true if Rails.env.development? && request.local?
+
ip_whitelist.any? { |e| e.include?(Gitlab::RequestContext.client_ip) }
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index e936d771502..316da8f129d 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -7,6 +7,10 @@ class GroupsController < Groups::ApplicationController
include PreviewMarkdown
include RecordUserLastActivity
+ before_action do
+ push_frontend_feature_flag(:manual_sorting)
+ end
+
respond_to :html
prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 7a1700a206a..ac1c4bc7fd3 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -46,18 +46,14 @@ class Projects::ForksController < Projects::ApplicationController
@forked_project ||= ::Projects::ForkService.new(project, current_user, namespace: namespace).execute
- if @forked_project.saved? && @forked_project.forked?
- if @forked_project.import_in_progress?
- redirect_to project_import_path(@forked_project, continue: continue_params)
- else
- if continue_params
- redirect_to continue_params[:to], notice: continue_params[:notice]
- else
- redirect_to project_path(@forked_project), notice: "The project '#{@forked_project.name}' was successfully forked."
- end
- end
- else
+ if !@forked_project.saved? || !@forked_project.forked?
render :error
+ elsif @forked_project.import_in_progress?
+ redirect_to project_import_path(@forked_project, continue: continue_params)
+ elsif continue_params[:to]
+ redirect_to continue_params[:to], notice: continue_params[:notice]
+ else
+ redirect_to project_path(@forked_project), notice: "The project '#{@forked_project.name}' was successfully forked."
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index afbf9fd7720..da32ab9e2e0 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -23,7 +23,7 @@ class Projects::ImportsController < Projects::ApplicationController
def show
if @project.import_finished?
- if continue_params&.key?(:to)
+ if continue_params[:to]
redirect_to continue_params[:to], notice: continue_params[:notice]
else
redirect_to project_path(@project), notice: finished_notice
@@ -31,11 +31,7 @@ class Projects::ImportsController < Projects::ApplicationController
elsif @project.import_failed?
redirect_to new_project_import_path(@project)
else
- if continue_params && continue_params[:notice_now]
- flash.now[:notice] = continue_params[:notice_now]
- end
-
- # Render
+ flash.now[:notice] = continue_params[:notice_now]
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b16f3dd9d82..f221f0363d3 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,6 +10,10 @@ class Projects::IssuesController < Projects::ApplicationController
include SpammableActions
include RecordUserLastActivity
+ before_action do
+ push_frontend_feature_flag(:manual_sorting)
+ end
+
def issue_except_actions
%i[index calendar new create bulk_update import_csv]
end
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index d7c0039b234..02ff6e872c9 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -103,7 +103,7 @@ class Projects::JobsController < Projects::ApplicationController
@build.cancel
- if continue_params
+ if continue_params[:to]
redirect_to continue_params[:to]
else
redirect_to builds_project_pipeline_path(@project, @build.pipeline.id)
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index b3447812ef2..b4ca9074ca9 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -55,6 +55,7 @@ class Projects::RefsController < Projects::ApplicationController
format.html { render_404 }
format.json do
response.headers["More-Logs-Url"] = @more_log_url if summary.more?
+ response.headers["More-Logs-Offset"] = summary.next_offset if summary.more?
render json: @logs
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 07b38371ab9..b2b151bbcf0 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -3,6 +3,7 @@
class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
include AcceptsPendingInvitations
+ include RecaptchaExperimentHelper
prepend_before_action :check_captcha, only: :create
before_action :whitelist_query_limiting, only: [:destroy]
@@ -15,13 +16,6 @@ class RegistrationsController < Devise::RegistrationsController
end
def create
- # To avoid duplicate form fields on the login page, the registration form
- # names fields using `new_user`, but Devise still wants the params in
- # `user`.
- if params["new_#{resource_name}"].present? && params[resource_name].blank?
- params[resource_name] = params.delete(:"new_#{resource_name}")
- end
-
accept_pending_invitations
super do |new_user|
@@ -74,19 +68,35 @@ class RegistrationsController < Devise::RegistrationsController
end
def after_sign_up_path_for(user)
- Gitlab::AppLogger.info("User Created: username=#{user.username} email=#{user.email} ip=#{request.remote_ip} confirmed:#{user.confirmed?}")
+ Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?))
user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path
end
def after_inactive_sign_up_path_for(resource)
- Gitlab::AppLogger.info("User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:false")
+ Gitlab::AppLogger.info(user_created_message)
users_almost_there_path
end
private
+ def user_created_message(confirmed: false)
+ "User Created: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip} confirmed:#{confirmed}"
+ end
+
+ def ensure_correct_params!
+ # To avoid duplicate form fields on the login page, the registration form
+ # names fields using `new_user`, but Devise still wants the params in
+ # `user`.
+ if params["new_#{resource_name}"].present? && params[resource_name].blank?
+ params[resource_name] = params.delete(:"new_#{resource_name}")
+ end
+ end
+
def check_captcha
- return unless Feature.enabled?(:registrations_recaptcha, default_enabled: true)
+ ensure_correct_params!
+
+ return unless Feature.enabled?(:registrations_recaptcha, default_enabled: true) # reCAPTCHA on the UI will still display however
+ return unless show_recaptcha_sign_up?
return unless Gitlab::Recaptcha.load_configurations!
return if verify_recaptcha
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 4469118f065..aaaa954047f 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -165,8 +165,6 @@ module ApplicationSettingsHelper
:authorized_keys_enabled,
:auto_devops_enabled,
:auto_devops_domain,
- :clientside_sentry_dsn,
- :clientside_sentry_enabled,
:container_registry_token_expire_delay,
:default_artifacts_expire_in,
:default_branch_protection,
@@ -235,8 +233,6 @@ module ApplicationSettingsHelper
:restricted_visibility_levels,
:rsa_key_restriction,
:send_user_confirmation_email,
- :sentry_dsn,
- :sentry_enabled,
:session_expire_delay,
:shared_runners_enabled,
:shared_runners_text,
@@ -253,6 +249,7 @@ module ApplicationSettingsHelper
:throttle_unauthenticated_enabled,
:throttle_unauthenticated_period_in_seconds,
:throttle_unauthenticated_requests_per_period,
+ :time_tracking_limit_to_hours,
:two_factor_grace_period,
:unique_ips_limit_enabled,
:unique_ips_limit_per_user,
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 1640f4fc93f..c5130b430b9 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -14,7 +14,8 @@ module BoardsHelper
issue_link_base: build_issue_link_base,
root_path: root_path,
bulk_update_path: @bulk_issues_path,
- default_avatar: image_path(default_avatar)
+ default_avatar: image_path(default_avatar),
+ time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s
}
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 150f24a5d5b..045de105b77 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -430,7 +430,8 @@ module IssuablesHelper
editable: issuable.dig(:current_user, :can_edit),
currentUser: issuable[:current_user],
rootPath: root_path,
- fullPath: issuable[:project_full_path]
+ fullPath: issuable[:project_full_path],
+ timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours
}
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 59332c0b100..dfadcfc33b2 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -5,6 +5,7 @@ module IssuesHelper
classes = ["issue"]
classes << "closed" if issue.closed?
classes << "today" if issue.today?
+ classes << "user-can-drag" if @sort == 'relative_position'
classes.join(' ')
end
diff --git a/app/helpers/recaptcha_experiment_helper.rb b/app/helpers/recaptcha_experiment_helper.rb
new file mode 100644
index 00000000000..d2eb9ac54f6
--- /dev/null
+++ b/app/helpers/recaptcha_experiment_helper.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module RecaptchaExperimentHelper
+ def show_recaptcha_sign_up?
+ !!Gitlab::Recaptcha.enabled?
+ end
+end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index dfa34ad7020..f5c4686a3bf 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -169,18 +169,17 @@ module SearchHelper
autocomplete: 'off'
}
+ opts[:data]['runner-tags-endpoint'] = tag_list_admin_runners_path
+
if @project.present?
opts[:data]['project-id'] = @project.id
- opts[:data]['base-endpoint'] = project_path(@project)
opts[:data]['labels-endpoint'] = project_labels_path(@project)
opts[:data]['milestones-endpoint'] = project_milestones_path(@project)
elsif @group.present?
opts[:data]['group-id'] = @group.id
- opts[:data]['base-endpoint'] = group_canonical_path(@group)
opts[:data]['labels-endpoint'] = group_labels_path(@group)
opts[:data]['milestones-endpoint'] = group_milestones_path(@group)
else
- opts[:data]['base-endpoint'] = root_dashboard_path
opts[:data]['labels-endpoint'] = dashboard_labels_path
opts[:data]['milestones-endpoint'] = dashboard_milestones_path
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index bbe2d2e8fd4..cd645850af3 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -30,6 +30,10 @@ class ApplicationSetting < ApplicationRecord
ignore_column :circuitbreaker_check_interval
ignore_column :koding_url
ignore_column :koding_enabled
+ ignore_column :sentry_enabled
+ ignore_column :sentry_dsn
+ ignore_column :clientside_sentry_enabled
+ ignore_column :clientside_sentry_dsn
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
@@ -75,14 +79,6 @@ class ApplicationSetting < ApplicationRecord
presence: true,
if: :recaptcha_enabled
- validates :sentry_dsn,
- presence: true,
- if: :sentry_enabled
-
- validates :clientside_sentry_dsn,
- presence: true,
- if: :clientside_sentry_enabled
-
validates :akismet_api_key,
presence: true,
if: :akismet_enabled
@@ -264,7 +260,6 @@ class ApplicationSetting < ApplicationRecord
encode: true
before_validation :ensure_uuid!
- before_validation :strip_sentry_values
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 904d650ef96..df4caed175d 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -82,6 +82,7 @@ module ApplicationSettingImplementation
throttle_unauthenticated_enabled: false,
throttle_unauthenticated_period_in_seconds: 3600,
throttle_unauthenticated_requests_per_period: 3600,
+ time_tracking_limit_to_hours: false,
two_factor_grace_period: 48,
unique_ips_limit_enabled: false,
unique_ips_limit_per_user: 10,
@@ -179,27 +180,6 @@ module ApplicationSettingImplementation
super(levels&.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end
- def strip_sentry_values
- sentry_dsn.strip! if sentry_dsn.present?
- clientside_sentry_dsn.strip! if clientside_sentry_dsn.present?
- end
-
- def sentry_enabled
- Gitlab.config.sentry.enabled || read_attribute(:sentry_enabled)
- end
-
- def sentry_dsn
- Gitlab.config.sentry.dsn || read_attribute(:sentry_dsn)
- end
-
- def clientside_sentry_enabled
- Gitlab.config.sentry.enabled || read_attribute(:clientside_sentry_enabled)
- end
-
- def clientside_sentry_dsn
- Gitlab.config.sentry.clientside_dsn || read_attribute(:clientside_sentry_dsn)
- end
-
def performance_bar_allowed_group
Group.find_by_id(performance_bar_allowed_group_id)
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 0fd8dca70b4..da4584228ce 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -45,7 +45,7 @@ class BroadcastMessage < ApplicationRecord
end
def self.cache_expires_in
- nil
+ 2.weeks
end
def active?
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index db7fd8524c2..f0256ff4d41 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.5.2'.freeze
+ VERSION = '0.6.0'.freeze
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index df2dc9c49eb..82034f5946b 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1030,9 +1030,9 @@ class MergeRequest < ApplicationRecord
def mergeable_ci_state?
return true unless project.only_allow_merge_if_pipeline_succeeds?
- return true unless head_pipeline
+ return false unless actual_head_pipeline
- actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
+ actual_head_pipeline.success? || actual_head_pipeline.skipped?
end
def environments_for(current_user)
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 3c270c7396a..f9b53b2b70a 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -35,6 +35,8 @@ class Namespace < ApplicationRecord
belongs_to :parent, class_name: "Namespace"
has_many :children, class_name: "Namespace", foreign_key: :parent_id
has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics'
+ has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule'
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
diff --git a/app/models/namespace/aggregation_schedule.rb b/app/models/namespace/aggregation_schedule.rb
new file mode 100644
index 00000000000..43afd0b954c
--- /dev/null
+++ b/app/models/namespace/aggregation_schedule.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class Namespace::AggregationSchedule < ApplicationRecord
+ self.primary_key = :namespace_id
+
+ belongs_to :namespace
+end
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
new file mode 100644
index 00000000000..de28eb6b37f
--- /dev/null
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class Namespace::RootStorageStatistics < ApplicationRecord
+ self.primary_key = :namespace_id
+
+ belongs_to :namespace
+ has_one :route, through: :namespace
+
+ delegate :all_projects, to: :namespace
+end
diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
index 3413a9e4612..58f795e639e 100644
--- a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
+++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
@@ -2,6 +2,14 @@
module PagesDomains
class ObtainLetsEncryptCertificateService
+ # time for processing validation requests for acme challenges
+ # 5-15 seconds is usually enough
+ CHALLENGE_PROCESSING_DELAY = 1.minute.freeze
+
+ # time LetsEncrypt ACME server needs to generate the certificate
+ # no particular SLA, usually takes 10-15 seconds
+ CERTIFICATE_PROCESSING_DELAY = 1.minute.freeze
+
attr_reader :pages_domain
def initialize(pages_domain)
@@ -14,6 +22,7 @@ module PagesDomains
unless acme_order
::PagesDomains::CreateAcmeOrderService.new(pages_domain).execute
+ PagesDomainSslRenewalWorker.perform_in(CHALLENGE_PROCESSING_DELAY, pages_domain.id)
return
end
@@ -23,6 +32,7 @@ module PagesDomains
case api_order.status
when 'ready'
api_order.request_certificate(private_key: acme_order.private_key, domain: pages_domain.domain)
+ PagesDomainSslRenewalWorker.perform_in(CERTIFICATE_PROCESSING_DELAY, pages_domain.id)
when 'valid'
save_certificate(acme_order.private_key, api_order)
acme_order.destroy!
diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_service_template.rb
index a2f36d2bd1b..a25c985585b 100644
--- a/app/services/projects/propagate_service_template.rb
+++ b/app/services/projects/propagate_service_template.rb
@@ -24,7 +24,7 @@ module Projects
def propagate_projects_with_template
loop do
- batch = project_ids_batch
+ batch = Project.uncached { project_ids_batch }
bulk_create_from_template(batch) unless batch.empty?
diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml
index bb4d1fa1241..e01c123d1db 100644
--- a/app/views/admin/application_settings/_localization.html.haml
+++ b/app/views/admin/application_settings/_localization.html.haml
@@ -8,4 +8,11 @@
.form-text.text-muted
= _('Default first day of the week in calendars and date pickers.')
+ .form-group
+ = f.label :time_tracking, _('Time tracking'), class: 'label-bold'
+ .form-check
+ = f.check_box :time_tracking_limit_to_hours, class: 'form-check-input'
+ = f.label :time_tracking_limit_to_hours, class: 'form-check-label' do
+ = _('Limit display of time tracking units to hours.')
+
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml
deleted file mode 100644
index d57066bba01..00000000000
--- a/app/views/admin/application_settings/_logging.html.haml
+++ /dev/null
@@ -1,38 +0,0 @@
-= form_for @application_setting, url: reporting_admin_application_settings_path(anchor: 'js-logging-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
-
- %p
- %strong
- NOTE:
- These settings will be removed from the UI in a GitLab 12.0 release and made available within gitlab.yml.
- In addition, you will be able to define a Sentry Environment to differentiate between multiple deployments. For example, development, staging, and production.
-
- %fieldset
- .form-group
- .form-check
- = f.check_box :sentry_enabled, class: 'form-check-input'
- = f.label :sentry_enabled, class: 'form-check-label' do
- Enable Sentry
- .form-text.text-muted
- %p This setting requires a restart to take effect.
- Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
- %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
-
- .form-group
- = f.label :sentry_dsn, 'Sentry DSN', class: 'label-bold'
- = f.text_field :sentry_dsn, class: 'form-control'
-
- .form-group
- .form-check
- = f.check_box :clientside_sentry_enabled, class: 'form-check-input'
- = f.label :clientside_sentry_enabled, class: 'form-check-label' do
- Enable Clientside Sentry
- .form-text.text-muted
- Sentry can also be used for reporting and logging clientside exceptions.
- %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
-
- .form-group
- = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'label-bold'
- = f.text_field :clientside_sentry_dsn, class: 'form-control'
-
- = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
index a34fc15acb1..d24e46b2815 100644
--- a/app/views/admin/application_settings/_spam.html.haml
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -7,7 +7,10 @@
= f.check_box :recaptcha_enabled, class: 'form-check-input'
= f.label :recaptcha_enabled, class: 'form-check-label' do
Enable reCAPTCHA
- %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts
+ - recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions'
+ - recaptcha_v2_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recaptcha_v2_link_url }
+ %span.form-text.text-muted#recaptcha_help_block
+ = _('Helps prevent bots from creating accounts. We currently only support %{recaptcha_v2_link_start}reCAPTCHA v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: '</a>'.html_safe }
.form-group
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-bold'
diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml
index 1c2d9ccdb2d..46e3d1c4570 100644
--- a/app/views/admin/application_settings/reporting.html.haml
+++ b/app/views/admin/application_settings/reporting.html.haml
@@ -23,14 +23,3 @@
= _('Set notification email for abuse reports.')
.settings-content
= render 'abuse'
-
-%section.settings.as-logging.no-animate#js-logging-settings{ class: ('expanded' if expanded_by_default?) }
- .settings-header
- %h4
- = _('Error Reporting and Logging')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded_by_default? ? _('Collapse') : _('Expand')
- %p
- = _('Enable Sentry for error reporting and logging.')
- .settings-content
- = render 'logging'
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 2e23b748edb..5129f5d193b 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -58,7 +58,7 @@
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
- %input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } }
+ %input.form-control.filtered-search{ search_filter_input_options('runners') }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index eae3ee6339f..034273558bb 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -33,7 +33,7 @@
= accept_terms_label.html_safe
= render_if_exists 'devise/shared/email_opted_in', f: f
%div
- - if Gitlab::Recaptcha.enabled?
+ - if show_recaptcha_sign_up?
= recaptcha_tags
.submit-container
= f.submit _("Register"), class: "btn-register btn qa-new-user-register-button"
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 7535aee83a3..20b844f9fd8 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -46,7 +46,7 @@
= yield :library_javascripts
= javascript_include_tag locale_path unless I18n.locale == :en
- = webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled
+ = webpack_bundle_tag "raven" if Gitlab.config.sentry.enabled
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 4c18398e3dc..65ef9690062 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title "Access Tokens"
-- page_title "Personal Access Tokens"
+- breadcrumb_title s_('AccessTokens|Access Tokens')
+- page_title s_('AccessTokens|Personal Access Tokens')
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
@@ -7,10 +7,10 @@
%h4.prepend-top-0
= page_title
%p
- You can generate a personal access token for each application you use that needs access to the GitLab API.
+ = s_('AccessTokens|You can generate a personal access token for each application you use that needs access to the GitLab API.')
%p
- You can also use personal access tokens to authenticate against Git over HTTP.
- They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.
+ = s_('AccessTokens|You can also use personal access tokens to authenticate against Git over HTTP.')
+ = s_('AccessTokens|They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.')
.col-lg-8
- if @new_personal_access_token
@@ -24,35 +24,33 @@
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- Feed token
+ = s_('AccessTokens|Feed token')
%p
- Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar, and is included in those feed URLs.
+ = s_('AccessTokens|Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar, and is included in those feed URLs.')
%p
- It cannot be used to access any other data.
+ = s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.feed-token-reset
- = label_tag :feed_token, 'Feed token', class: "label-bold"
+ = label_tag :feed_token, s_('AccessTokens|Feed token'), class: "label-bold"
= text_field_tag :feed_token, current_user.feed_token, class: 'form-control', readonly: true, onclick: 'this.select()'
%p.form-text.text-muted
- Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you.
- You should
- = link_to 'reset it', [:reset, :feed_token, :profile], method: :put, data: { confirm: 'Are you sure? Any RSS or calendar URLs currently in use will stop working.' }
- if that ever happens.
+ - reset_link = link_to s_('AccessTokens|reset it'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.') }
+ - reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
+ = reset_message.html_safe
- if incoming_email_token_enabled?
%hr
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
- Incoming email token
+ = s_('AccessTokens|Incoming email token')
%p
- Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses.
+ = s_('AccessTokens|Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses.')
%p
- It cannot be used to access any other data.
+ = s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.incoming-email-token-reset
- = label_tag :incoming_email_token, 'Incoming email token', class: "label-bold"
+ = label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: "label-bold"
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()'
%p.form-text.text-muted
- Keep this token secret. Anyone who gets ahold of it can create issues as if they were you.
- You should
- = link_to 'reset it', [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: 'Are you sure? Any issue email addresses currently in use will stop working.' }
- if that ever happens.
+ - reset_link = link_to s_('AccessTokens|reset it'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.') }
+ - reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link }
+ = reset_message.html_safe
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index 6f7713124ac..7d539c9d749 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,6 +1,6 @@
- empty_state_path = local_assigns.fetch(:empty_state_path, 'shared/empty_states/issues')
-%ul.content-list.issues-list.issuable-list
+%ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position') }
= render partial: "projects/issues/issue", collection: @issues
- if @issues.blank?
= render empty_state_path
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index d7e16dbd40c..1cfe302fdc7 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -46,11 +46,11 @@
= render_if_exists 'projects/new_ci_cd_only_project_tab', active_tab: active_tab
.tab-content.gitlab-tab-content
- .tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
+ .tab-pane.js-toggle-container{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
- #create-from-template-pane.tab-pane.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' }
+ #create-from-template-pane.tab-pane.js-toggle-container.px-0.pb-0{ class: active_when(active_tab == 'template'), role: 'tabpanel' }
.card-slim.m-4.p-4
%div
- contributing_templates_url = 'https://gitlab.com/gitlab-org/project-templates/contributing'
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index e7edb93f05b..5b657966909 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -11,7 +11,7 @@
- if Gitlab.config.pages.external_https
- - auto_ssl_available = ::Gitlab::LetsEncrypt::Client.new.enabled?
+ - auto_ssl_available = ::Gitlab::LetsEncrypt.enabled?(@domain)
- auto_ssl_enabled = @domain.auto_ssl_enabled?
- auto_ssl_available_and_enabled = auto_ssl_available && auto_ssl_enabled
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 987a5d4f13f..a21dcabb485 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -1,6 +1,6 @@
- if @issues.to_a.any?
.card.card-small.card-without-border
- %ul.content-list.issues-list.issuable-list
+ %ul.content-list.issues-list.issuable-list{ class: ("manual-ordering" if @sort == 'relative_position'), data: { group_full_path: @group&.full_path } }
= render partial: 'projects/issues/issue', collection: @issues
= paginate @issues, theme: "gitlab"
- else
diff --git a/app/views/shared/_personal_access_tokens_created_container.html.haml b/app/views/shared/_personal_access_tokens_created_container.html.haml
index a8d3de66418..42989b145a2 100644
--- a/app/views/shared/_personal_access_tokens_created_container.html.haml
+++ b/app/views/shared/_personal_access_tokens_created_container.html.haml
@@ -1,5 +1,5 @@
-- container_title = local_assigns.fetch(:container_title, 'Your New Personal Access Token')
-- clipboard_button_title = local_assigns.fetch(:clipboard_button_title, 'Copy personal access token to clipboard')
+- container_title = local_assigns.fetch(:container_title, _('Your New Personal Access Token'))
+- clipboard_button_title = local_assigns.fetch(:clipboard_button_title, _('Copy personal access token to clipboard'))
.created-personal-access-token-container
%h5.prepend-top-0
@@ -9,6 +9,7 @@
= text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "qa-created-personal-access-token form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block"
%span.input-group-append
= clipboard_button(text: new_token_value, title: clipboard_button_title, placement: "left", class: "input-group-text btn-default btn-clipboard")
- %span#created-token-help-block.form-text.text-muted.text-danger Make sure you save it - you won't be able to access it again.
+ %span#created-token-help-block.form-text.text-muted.text-danger
+ = _("Make sure you save it - you won't be able to access it again.")
%hr
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
index 0891b3459ec..1d96feda3b0 100644
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ b/app/views/shared/_personal_access_tokens_form.html.haml
@@ -1,9 +1,9 @@
-- type = impersonation ? "impersonation" : "personal access"
+- type = impersonation ? s_('Profiles|impersonation') : s_('Profiles|personal access')
%h5.prepend-top-0
- Add a #{type} token
+ = _('Add a %{type} token') % { type: type }
%p.profile-settings-content
- Pick a name for the application, and we'll give you a unique #{type} token.
+ = _("Pick a name for the application, and we'll give you a unique %{type} token.") % { type: type }
= form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
@@ -11,19 +11,19 @@
.row
.form-group.col-md-6
- = f.label :name, class: 'label-bold'
+ = f.label :name, _('Name'), class: 'label-bold'
= f.text_field :name, class: "form-control qa-personal-access-token-name-field", required: true
.row
.form-group.col-md-6
- = f.label :expires_at, class: 'label-bold'
+ = f.label :expires_at, _('Expires at'), class: 'label-bold'
.input-icon-wrapper
= f.text_field :expires_at, class: "datepicker form-control", placeholder: 'YYYY-MM-DD'
= icon('calendar', { class: 'input-icon-right' })
.form-group
- = f.label :scopes, class: 'label-bold'
+ = f.label :scopes, _('Scopes'), class: 'label-bold'
= render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes
.prepend-top-default
- = f.submit "Create #{type} token", class: "btn btn-success qa-create-token-button"
+ = f.submit _('Create %{type} token') % { type: type }, class: "btn btn-success qa-create-token-button"
diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml
index 49f3aae0f98..823117f37ca 100644
--- a/app/views/shared/_personal_access_tokens_table.html.haml
+++ b/app/views/shared/_personal_access_tokens_table.html.haml
@@ -1,20 +1,21 @@
-- type = impersonation ? "Impersonation" : "Personal Access"
+- type = impersonation ? s_('Profiles|Impersonation') : s_('Profiles|Personal Access')
%hr
-%h5 Active #{type} Tokens (#{active_tokens.length})
+%h5
+ = _('Active %{type} Tokens (%{token_length})') % { type: type, token_length: active_tokens.length }
- if impersonation
%p.profile-settings-content
- To see all the user's personal access tokens you must impersonate them first.
+ = _("To see all the user's personal access tokens you must impersonate them first.")
- if active_tokens.present?
.table-responsive
%table.table.active-tokens
%thead
%tr
- %th Name
- %th Created
- %th Expires
- %th Scopes
+ %th= _('Name')
+ %th= s_('AccessTokens|Created')
+ %th= _('Expires')
+ %th= _('Scopes')
%th
%tbody
- active_tokens.each do |token|
@@ -26,10 +27,10 @@
%span{ class: ('text-warning' if token.expires_soon?) }
In #{distance_of_time_in_words_to_now(token.expires_at)}
- else
- %span.token-never-expires-label Never
- %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
+ %span.token-never-expires-label= _('Never')
+ %td= token.scopes.present? ? token.scopes.join(", ") : _('<no scopes selected>')
- path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token)
- %td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right qa-revoke-button", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." }
+ %td= link_to _('Revoke'), path, method: :put, class: "btn btn-danger float-right qa-revoke-button", data: { confirm: _('Are you sure you want to revoke this %{type} Token? This action cannot be undone.') % { type: type } }
- else
.settings-message.text-center
- This user has no active #{type} Tokens.
+ = _('This user has no active %{type} Tokens.') % { type: type }
diff --git a/app/views/shared/boards/components/sidebar/_time_tracker.html.haml b/app/views/shared/boards/components/sidebar/_time_tracker.html.haml
index b76d44c5907..43081499920 100644
--- a/app/views/shared/boards/components/sidebar/_time_tracker.html.haml
+++ b/app/views/shared/boards/components/sidebar/_time_tracker.html.haml
@@ -3,4 +3,5 @@
":time-spent" => "issue.timeSpent || 0",
":human-time-estimate" => "issue.humanTimeEstimate",
":human-time-spent" => "issue.humanTimeSpent",
+ ":limit-to-hours" => "timeTrackingLimitToHours",
"root-path" => "#{root_url}" }
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index 1dd97bc4ed1..403e001bfe8 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -1,6 +1,7 @@
- sort_value = @sort
- sort_title = issuable_sort_option_title(sort_value)
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
+- manual_sorting = viewing_issues && controller.controller_name != 'dashboard' && Feature.enabled?(:manual_sorting)
.dropdown.inline.prepend-left-10.issue-sort-dropdown
.btn-group{ role: 'group' }
@@ -17,6 +18,6 @@
= sortable_item(sort_title_due_date, page_filter_path(sort: sort_value_due_date), sort_title) if viewing_issues
= sortable_item(sort_title_popularity, page_filter_path(sort: sort_value_popularity), sort_title)
= sortable_item(sort_title_label_priority, page_filter_path(sort: sort_value_label_priority), sort_title)
- = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if viewing_issues && Feature.enabled?(:manual_sorting)
+ = sortable_item(sort_title_relative_position, page_filter_path(sort: sort_value_relative_position), sort_title) if manual_sorting
= render_if_exists('shared/ee/issuable/sort_dropdown', viewing_issues: viewing_issues, sort_title: sort_title)
= issuable_sort_direction_button(sort_value)
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index b24075c7849..ced6af50501 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -93,7 +93,11 @@
= milestone.issues_visible_to_user(current_user).closed.count
.block
- #issuable-time-tracker{ data: { time_estimate: @milestone.total_issue_time_estimate, time_spent: @milestone.total_issue_time_spent, human_time_estimate: @milestone.human_total_issue_time_estimate, human_time_spent: @milestone.human_total_issue_time_spent } }
+ #issuable-time-tracker{ data: { time_estimate: @milestone.total_issue_time_estimate,
+ time_spent: @milestone.total_issue_time_spent,
+ human_time_estimate: @milestone.human_total_issue_time_estimate,
+ human_time_spent: @milestone.human_total_issue_time_spent,
+ limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s } }
// Fallback while content is loading
.title.hide-collapsed
= _('Time tracking')
diff --git a/app/views/shared/tokens/_scopes_list.html.haml b/app/views/shared/tokens/_scopes_list.html.haml
index f99e905e95c..428861485b4 100644
--- a/app/views/shared/tokens/_scopes_list.html.haml
+++ b/app/views/shared/tokens/_scopes_list.html.haml
@@ -4,7 +4,7 @@
%tr
%td
- Scopes
+ = _('Scopes')
%td
%ul.scopes-list.append-bottom-0
- token.scopes.each do |scope|
diff --git a/app/workers/pages_domain_ssl_renewal_cron_worker.rb b/app/workers/pages_domain_ssl_renewal_cron_worker.rb
index 4ca9db922b4..40c34d29970 100644
--- a/app/workers/pages_domain_ssl_renewal_cron_worker.rb
+++ b/app/workers/pages_domain_ssl_renewal_cron_worker.rb
@@ -5,9 +5,9 @@ class PagesDomainSslRenewalCronWorker
include CronjobQueue
def perform
- return unless ::Gitlab::LetsEncrypt::Client.new.enabled?
-
PagesDomain.need_auto_ssl_renewal.find_each do |domain|
+ next unless ::Gitlab::LetsEncrypt.enabled?(domain)
+
PagesDomainSslRenewalWorker.perform_async(domain.id)
end
end
diff --git a/app/workers/pages_domain_ssl_renewal_worker.rb b/app/workers/pages_domain_ssl_renewal_worker.rb
index 00c9c4782d8..b32458ca777 100644
--- a/app/workers/pages_domain_ssl_renewal_worker.rb
+++ b/app/workers/pages_domain_ssl_renewal_worker.rb
@@ -4,11 +4,9 @@ class PagesDomainSslRenewalWorker
include ApplicationWorker
def perform(domain_id)
- return unless ::Gitlab::LetsEncrypt::Client.new.enabled?
-
domain = PagesDomain.find_by_id(domain_id)
-
- return unless domain
+ return unless domain&.enabled?
+ return unless ::Gitlab::LetsEncrypt.enabled?(domain)
::PagesDomains::ObtainLetsEncryptCertificateService.new(domain).execute
end
diff --git a/changelogs/unreleased/30355-use-hours-only-for-time-tracking.yml b/changelogs/unreleased/30355-use-hours-only-for-time-tracking.yml
new file mode 100644
index 00000000000..b0252f9e81b
--- /dev/null
+++ b/changelogs/unreleased/30355-use-hours-only-for-time-tracking.yml
@@ -0,0 +1,5 @@
+---
+title: Add option to limit time tracking units to hours
+merge_request: 29469
+author: Jon Kolb
+type: added
diff --git a/changelogs/unreleased/51952-forking-via-webide.yml b/changelogs/unreleased/51952-forking-via-webide.yml
new file mode 100644
index 00000000000..4497c6b6ca4
--- /dev/null
+++ b/changelogs/unreleased/51952-forking-via-webide.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve "500 error when forking via the web IDE button"
+merge_request: 29909
+author:
+type: fixed
diff --git a/changelogs/unreleased/58802-rename-webide.yml b/changelogs/unreleased/58802-rename-webide.yml
new file mode 100644
index 00000000000..40471d967ce
--- /dev/null
+++ b/changelogs/unreleased/58802-rename-webide.yml
@@ -0,0 +1,5 @@
+---
+title: Re-name files in Web IDE in a more natural way
+merge_request: 29948
+author:
+type: changed
diff --git a/changelogs/unreleased/59702-fix-notification-flags-for-ms-teams.yml b/changelogs/unreleased/59702-fix-notification-flags-for-ms-teams.yml
deleted file mode 100644
index 14a8da95ed9..00000000000
--- a/changelogs/unreleased/59702-fix-notification-flags-for-ms-teams.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix missing API notification flags for Microsoft Teams
-merge_request: 29824
-author: Seiji Suenaga
-type: fixed
diff --git a/changelogs/unreleased/60860-keep-empty-folders-in-tree.yml b/changelogs/unreleased/60860-keep-empty-folders-in-tree.yml
new file mode 100644
index 00000000000..237d0fd6aef
--- /dev/null
+++ b/changelogs/unreleased/60860-keep-empty-folders-in-tree.yml
@@ -0,0 +1,5 @@
+---
+title: Keep the empty folders in the tree
+merge_request: 29196
+author:
+type: fixed
diff --git a/changelogs/unreleased/60879-fix-reports-timing-out.yml b/changelogs/unreleased/60879-fix-reports-timing-out.yml
new file mode 100644
index 00000000000..845162fe10f
--- /dev/null
+++ b/changelogs/unreleased/60879-fix-reports-timing-out.yml
@@ -0,0 +1,5 @@
+---
+title: Fix reports jobs timing out because of cache
+merge_request: 29780
+author:
+type: fixed
diff --git a/changelogs/unreleased/62938-wcag-aa-edited-text-color.yml b/changelogs/unreleased/62938-wcag-aa-edited-text-color.yml
new file mode 100644
index 00000000000..6652e495869
--- /dev/null
+++ b/changelogs/unreleased/62938-wcag-aa-edited-text-color.yml
@@ -0,0 +1,5 @@
+---
+title: Use darker gray color for system note metadata and edited text
+merge_request: 30054
+author:
+type: other
diff --git a/changelogs/unreleased/63247-add-conf-toast-and-link.yml b/changelogs/unreleased/63247-add-conf-toast-and-link.yml
new file mode 100644
index 00000000000..915cc20dcc8
--- /dev/null
+++ b/changelogs/unreleased/63247-add-conf-toast-and-link.yml
@@ -0,0 +1,5 @@
+---
+title: Include a link back to the MR for Visual Review feedback form
+merge_request: 29719
+author:
+type: changed
diff --git a/changelogs/unreleased/63513-ensure-gitlab-jsoncache-includes-the-gitlab-version-in-the-cache-key.yml b/changelogs/unreleased/63513-ensure-gitlab-jsoncache-includes-the-gitlab-version-in-the-cache-key.yml
deleted file mode 100644
index b5715902630..00000000000
--- a/changelogs/unreleased/63513-ensure-gitlab-jsoncache-includes-the-gitlab-version-in-the-cache-key.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Include the GitLab version in the cache key for Gitlab::JsonCache
-merge_request: 29938
-author:
-type: fixed
diff --git a/changelogs/unreleased/always-allow-prometheus-access-in-dev.yml b/changelogs/unreleased/always-allow-prometheus-access-in-dev.yml
new file mode 100644
index 00000000000..acd944ea684
--- /dev/null
+++ b/changelogs/unreleased/always-allow-prometheus-access-in-dev.yml
@@ -0,0 +1,5 @@
+---
+title: Always allow access to health endpoints from localhost in dev
+merge_request: 29930
+author:
+type: other
diff --git a/changelogs/unreleased/always-display-environment-selector.yml b/changelogs/unreleased/always-display-environment-selector.yml
new file mode 100644
index 00000000000..7a55e8f3e5d
--- /dev/null
+++ b/changelogs/unreleased/always-display-environment-selector.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken environment selector and always display it on monitoring dashboard
+merge_request: 29705
+author:
+type: fixed
diff --git a/changelogs/unreleased/bug-63162-duplicate_path_in_links.yml b/changelogs/unreleased/bug-63162-duplicate_path_in_links.yml
deleted file mode 100644
index d3f246492fb..00000000000
--- a/changelogs/unreleased/bug-63162-duplicate_path_in_links.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed 'diff version changes' link not working
-merge_request: 29825
-author:
-type: fixed
diff --git a/changelogs/unreleased/fe-issue-reorder.yml b/changelogs/unreleased/fe-issue-reorder.yml
new file mode 100644
index 00000000000..aca334b6149
--- /dev/null
+++ b/changelogs/unreleased/fe-issue-reorder.yml
@@ -0,0 +1,5 @@
+---
+title: Bring Manual Ordering on Issue List
+merge_request: 29410
+author:
+type: added
diff --git a/changelogs/unreleased/fix-jupyter-git-v3.yml b/changelogs/unreleased/fix-jupyter-git-v3.yml
new file mode 100644
index 00000000000..8aaaaf249fb
--- /dev/null
+++ b/changelogs/unreleased/fix-jupyter-git-v3.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Jupyter-Git integration
+merge_request: 30020
+author: Amit Rathi
+type: fixed
diff --git a/changelogs/unreleased/fix-labels-in-hooks.yml b/changelogs/unreleased/fix-labels-in-hooks.yml
deleted file mode 100644
index c0904a860c5..00000000000
--- a/changelogs/unreleased/fix-labels-in-hooks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix label serialization in issue and note hooks
-merge_request: 29850
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-notes-emails-with-group-settings.yml b/changelogs/unreleased/fix-notes-emails-with-group-settings.yml
deleted file mode 100644
index 77dae8418a8..00000000000
--- a/changelogs/unreleased/fix-notes-emails-with-group-settings.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix comment emails not respecting group-level notification email
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/gitaly-version-v1.49.0.yml b/changelogs/unreleased/gitaly-version-v1.49.0.yml
new file mode 100644
index 00000000000..8795bab0209
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.49.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.49.0
+merge_request: 29990
+author:
+type: changed
diff --git a/changelogs/unreleased/mh-colon-autocomplete.yml b/changelogs/unreleased/mh-colon-autocomplete.yml
new file mode 100644
index 00000000000..8b169c22588
--- /dev/null
+++ b/changelogs/unreleased/mh-colon-autocomplete.yml
@@ -0,0 +1,5 @@
+---
+title: Allow auto-completing scoped labels
+merge_request: 29749
+author:
+type: added
diff --git a/changelogs/unreleased/refactor-sentry.yml b/changelogs/unreleased/refactor-sentry.yml
new file mode 100644
index 00000000000..25c5534fae0
--- /dev/null
+++ b/changelogs/unreleased/refactor-sentry.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Sentry from application settings
+merge_request: 28447
+author: Roger Meier
+type: added
diff --git a/changelogs/unreleased/require-pipeline-when-enabling-only-allow-merge-if-pipeline-succeeds.yml b/changelogs/unreleased/require-pipeline-when-enabling-only-allow-merge-if-pipeline-succeeds.yml
new file mode 100644
index 00000000000..c105287532b
--- /dev/null
+++ b/changelogs/unreleased/require-pipeline-when-enabling-only-allow-merge-if-pipeline-succeeds.yml
@@ -0,0 +1,5 @@
+---
+title: Enforce presence of pipeline when "Pipeline must succeed" project setting is enabled
+merge_request: 29926
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-cache-negative-entries-find-commit.yml b/changelogs/unreleased/sh-cache-negative-entries-find-commit.yml
new file mode 100644
index 00000000000..98eb13ee620
--- /dev/null
+++ b/changelogs/unreleased/sh-cache-negative-entries-find-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Allow caching of negative FindCommit matches
+merge_request: 29952
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-omit-issues-links-on-poll.yml b/changelogs/unreleased/sh-omit-issues-links-on-poll.yml
deleted file mode 100644
index 21e51d3534f..00000000000
--- a/changelogs/unreleased/sh-omit-issues-links-on-poll.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Omit issues links in merge request entity API response
-merge_request: 29917
-author:
-type: performance
diff --git a/changelogs/unreleased/sh-service-template-bug.yml b/changelogs/unreleased/sh-service-template-bug.yml
new file mode 100644
index 00000000000..1ea5ac84f26
--- /dev/null
+++ b/changelogs/unreleased/sh-service-template-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Disable Rails SQL query cache when applying service templates
+merge_request: 30060
+author:
+type: fixed
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-6-0.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-6-0.yml
new file mode 100644
index 00000000000..6719fa94b19
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-6-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Runner Helm Chart to 0.6.0
+merge_request: 29982
+author:
+type: other
diff --git a/config/initializers/0_inflections.rb b/config/initializers/0_inflections.rb
index 1ad9ddca877..4d1f4917275 100644
--- a/config/initializers/0_inflections.rb
+++ b/config/initializers/0_inflections.rb
@@ -14,6 +14,14 @@ ActiveSupport::Inflector.inflections do |inflect|
award_emoji
project_statistics
system_note_metadata
+ event_log
project_auto_devops
+ project_registry
+ file_registry
+ job_artifact_registry
+ vulnerability_feedback
+ vulnerabilities_feedback
+ group_view
)
+ inflect.acronym 'EE'
end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4b0bb86e42a..9e74a67b73f 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -368,7 +368,7 @@ Settings.cron_jobs['pages_domain_removal_cron_worker']['cron'] ||= '47 0 * * *'
Settings.cron_jobs['pages_domain_removal_cron_worker']['job_class'] = 'PagesDomainRemovalCronWorker'
Settings.cron_jobs['pages_domain_ssl_renewal_cron_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['pages_domain_ssl_renewal_cron_worker']['cron'] ||= '*/5 * * * *'
+Settings.cron_jobs['pages_domain_ssl_renewal_cron_worker']['cron'] ||= '*/10 * * * *'
Settings.cron_jobs['pages_domain_ssl_renewal_cron_worker']['job_class'] = 'PagesDomainSslRenewalCronWorker'
Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({})
diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb
index deb94d7dbce..a69f1ba090e 100644
--- a/config/initializers/forbid_sidekiq_in_transactions.rb
+++ b/config/initializers/forbid_sidekiq_in_transactions.rb
@@ -17,7 +17,7 @@ module Sidekiq
module NoEnqueueingFromTransactions
%i(perform_async perform_at perform_in).each do |name|
define_method(name) do |*args|
- if !Sidekiq::Worker.skip_transaction_check && AfterCommitQueue.inside_transaction?
+ if !Sidekiq::Worker.skip_transaction_check && Gitlab::Database.inside_transaction?
begin
raise Sidekiq::Worker::EnqueueFromTransactionError, <<~MSG
`#{self}.#{name}` cannot be called inside a transaction as this can lead to
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index e5589ce0ad1..fcc6bfa5c92 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -3,18 +3,11 @@
require 'gitlab/current_settings'
def configure_sentry
- # allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done
- begin
- sentry_enabled = Gitlab::CurrentSettings.current_application_settings.sentry_enabled
- rescue
- sentry_enabled = false
- end
-
- if sentry_enabled
+ if Gitlab::Sentry.enabled?
Raven.configure do |config|
- config.dsn = Gitlab::CurrentSettings.current_application_settings.sentry_dsn
+ config.dsn = Gitlab.config.sentry.dsn
config.release = Gitlab.revision
- config.current_environment = Gitlab.config.sentry.environment.presence
+ config.current_environment = Gitlab.config.sentry.environment
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
diff --git a/db/migrate/20190402150158_backport_enterprise_schema.rb b/db/migrate/20190402150158_backport_enterprise_schema.rb
index 610a8808383..8762cc53ed7 100644
--- a/db/migrate/20190402150158_backport_enterprise_schema.rb
+++ b/db/migrate/20190402150158_backport_enterprise_schema.rb
@@ -117,6 +117,8 @@ class BackportEnterpriseSchema < ActiveRecord::Migration[5.0]
end
def up
+ check_schema!
+
create_missing_tables
update_appearances
@@ -868,6 +870,52 @@ class BackportEnterpriseSchema < ActiveRecord::Migration[5.0]
remove_column_if_exists(:geo_nodes, :internal_url)
end
+ # Some users may have upgraded to EE at some point but downgraded to
+ # CE v11.11.3. As a result, their EE tables may not be in the right
+ # state. Here we check for these such cases and attempt to guide the
+ # user into recovering from this state by upgrading to v11.11.3 EE
+ # before installing v12.0.0 CE.
+ def check_schema!
+ # The following cases will fail later when this migration attempts
+ # to add a foreign key for non-existent columns.
+ columns_to_check = [
+ [:epics, :parent_id], # Added in GitLab 11.7
+ [:geo_event_log, :cache_invalidation_event_id], # Added in GitLab 11.4
+ [:vulnerability_feedback, :merge_request_id] # Added in GitLab 11.9
+ ].freeze
+
+ columns_to_check.each do |table, column|
+ check_ee_columns!(table, column)
+ end
+ end
+
+ def check_ee_columns!(table, column)
+ return unless table_exists?(table)
+ return if column_exists?(table, column)
+
+ raise_ee_migration_error!(table, column)
+ end
+
+ def raise_ee_migration_error!(table, column)
+ message = "Your database is missing the '#{column}' column from the '#{table}' table that is present for GitLab EE."
+
+ message +=
+ if ::Gitlab.ee?
+ "\nUpgrade your GitLab instance to 11.11.3 EE first!"
+ else
+ <<~MSG
+
+ Even though it looks like you're running a CE installation, it appears
+ you may have installed GitLab EE at some point. To migrate to GitLab 12.0:
+
+ 1. Install GitLab 11.11.3 EE
+ 2. Install GitLab 12.0.x CE
+ MSG
+ end
+
+ raise Exception.new(message)
+ end
+
def create_missing_tables
create_table_if_not_exists "approval_merge_request_rule_sources", id: :bigserial do |t|
t.bigint "approval_merge_request_rule_id", null: false
diff --git a/db/migrate/20190513174947_enable_create_incident_issues_by_default.rb b/db/migrate/20190513174947_enable_create_incident_issues_by_default.rb
new file mode 100644
index 00000000000..ecd466627fe
--- /dev/null
+++ b/db/migrate/20190513174947_enable_create_incident_issues_by_default.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class EnableCreateIncidentIssuesByDefault < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ change_default_for :create_issue, from: false, to: true
+ change_default_for :send_email, from: true, to: false
+ end
+
+ private
+
+ def change_default_for(column, from:, to:)
+ change_column_default :project_incident_management_settings,
+ column, from: from, to: to
+ end
+end
diff --git a/db/migrate/20190531153110_create_namespace_root_storage_statistics.rb b/db/migrate/20190531153110_create_namespace_root_storage_statistics.rb
new file mode 100644
index 00000000000..702560d05cc
--- /dev/null
+++ b/db/migrate/20190531153110_create_namespace_root_storage_statistics.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class CreateNamespaceRootStorageStatistics < ActiveRecord::Migration[5.1]
+ DOWNTIME = false
+
+ def change
+ create_table :namespace_root_storage_statistics, id: false, primary_key: :namespace_id do |t|
+ t.integer :namespace_id, null: false, primary_key: true
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.bigint :repository_size, null: false, default: 0
+ t.bigint :lfs_objects_size, null: false, default: 0
+ t.bigint :wiki_size, null: false, default: 0
+ t.bigint :build_artifacts_size, null: false, default: 0
+ t.bigint :storage_size, null: false, default: 0
+ t.bigint :packages_size, null: false, default: 0
+
+ t.index :namespace_id, unique: true
+ t.foreign_key :namespaces, column: :namespace_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/migrate/20190605184422_create_namespace_aggregation_schedules.rb b/db/migrate/20190605184422_create_namespace_aggregation_schedules.rb
new file mode 100644
index 00000000000..5e8cb616cc1
--- /dev/null
+++ b/db/migrate/20190605184422_create_namespace_aggregation_schedules.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateNamespaceAggregationSchedules < ActiveRecord::Migration[5.1]
+ DOWNTIME = false
+
+ def change
+ create_table :namespace_aggregation_schedules, id: false, primary_key: :namespace_id do |t|
+ t.integer :namespace_id, null: false, primary_key: true
+
+ t.index :namespace_id, unique: true
+ t.foreign_key :namespaces, column: :namespace_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/migrate/20190611090827_add_time_tracking_limit_to_hours_to_application_settings.rb b/db/migrate/20190611090827_add_time_tracking_limit_to_hours_to_application_settings.rb
new file mode 100644
index 00000000000..a5f8925c1db
--- /dev/null
+++ b/db/migrate/20190611090827_add_time_tracking_limit_to_hours_to_application_settings.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTimeTrackingLimitToHoursToApplicationSettings < ActiveRecord::Migration[5.1]
+ 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 :application_settings, :time_tracking_limit_to_hours, :boolean, default: false, allow_null: false
+ end
+
+ def down
+ remove_column :application_settings, :time_tracking_limit_to_hours
+ end
+end
diff --git a/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb b/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb
new file mode 100644
index 00000000000..427df343193
--- /dev/null
+++ b/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveSentryFromApplicationSettings < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ SENTRY_ENABLED_COLUMNS = [
+ :sentry_enabled,
+ :clientside_sentry_enabled
+ ].freeze
+
+ SENTRY_DSN_COLUMNS = [
+ :sentry_dsn,
+ :clientside_sentry_dsn
+ ].freeze
+
+ disable_ddl_transaction!
+
+ def up
+ (SENTRY_ENABLED_COLUMNS + SENTRY_DSN_COLUMNS).each do |column|
+ remove_column(:application_settings, column) if column_exists?(:application_settings, column)
+ end
+ end
+
+ def down
+ SENTRY_ENABLED_COLUMNS.each do |column|
+ add_column_with_default(:application_settings, column, :boolean, default: false, allow_null: false) unless column_exists?(:application_settings, column)
+ end
+
+ SENTRY_DSN_COLUMNS.each do |column|
+ add_column(:application_settings, column, :string) unless column_exists?(:application_settings, column)
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b81558178b9..054dbc7201f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20190620112608) do
+ActiveRecord::Schema.define(version: 20190625184066) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -93,8 +93,6 @@ ActiveRecord::Schema.define(version: 20190620112608) do
t.boolean "akismet_enabled", default: false
t.string "akismet_api_key"
t.integer "metrics_sample_interval", default: 15
- t.boolean "sentry_enabled", default: false
- t.string "sentry_dsn"
t.boolean "email_author_in_body", default: false
t.integer "default_group_visibility"
t.boolean "repository_checks_enabled", default: false
@@ -135,8 +133,6 @@ ActiveRecord::Schema.define(version: 20190620112608) do
t.string "uuid"
t.decimal "polling_interval_multiplier", default: "1.0", null: false
t.integer "cached_markdown_version"
- t.boolean "clientside_sentry_enabled", default: false, null: false
- t.string "clientside_sentry_dsn"
t.boolean "prometheus_metrics_enabled", default: true, null: false
t.boolean "help_page_hide_commercial_content", default: false
t.string "help_page_support_url"
@@ -229,6 +225,7 @@ ActiveRecord::Schema.define(version: 20190620112608) do
t.integer "custom_project_templates_group_id"
t.boolean "elasticsearch_limit_indexing", default: false, null: false
t.string "geo_node_allowed_ips", default: "0.0.0.0/0, ::/0"
+ t.boolean "time_tracking_limit_to_hours", default: false, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
@@ -2054,6 +2051,21 @@ ActiveRecord::Schema.define(version: 20190620112608) do
t.index ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
end
+ create_table "namespace_aggregation_schedules", primary_key: "namespace_id", id: :integer, default: nil, force: :cascade do |t|
+ t.index ["namespace_id"], name: "index_namespace_aggregation_schedules_on_namespace_id", unique: true, using: :btree
+ end
+
+ create_table "namespace_root_storage_statistics", primary_key: "namespace_id", id: :integer, default: nil, force: :cascade do |t|
+ t.datetime_with_timezone "updated_at", null: false
+ t.bigint "repository_size", default: 0, null: false
+ t.bigint "lfs_objects_size", default: 0, null: false
+ t.bigint "wiki_size", default: 0, null: false
+ t.bigint "build_artifacts_size", default: 0, null: false
+ t.bigint "storage_size", default: 0, null: false
+ t.bigint "packages_size", default: 0, null: false
+ t.index ["namespace_id"], name: "index_namespace_root_storage_statistics_on_namespace_id", unique: true, using: :btree
+ end
+
create_table "namespace_statistics", id: :serial, force: :cascade do |t|
t.integer "namespace_id", null: false
t.integer "shared_runners_seconds", default: 0, null: false
@@ -2502,9 +2514,9 @@ ActiveRecord::Schema.define(version: 20190620112608) do
t.index ["project_id"], name: "index_project_import_data_on_project_id", using: :btree
end
- create_table "project_incident_management_settings", primary_key: "project_id", id: :integer, default: nil, force: :cascade do |t|
- t.boolean "create_issue", default: false, null: false
- t.boolean "send_email", default: true, null: false
+ create_table "project_incident_management_settings", primary_key: "project_id", id: :serial, force: :cascade do |t|
+ t.boolean "create_issue", default: true, null: false
+ t.boolean "send_email", default: false, null: false
t.text "issue_template_key"
end
@@ -3756,6 +3768,8 @@ ActiveRecord::Schema.define(version: 20190620112608) do
add_foreign_key "merge_trains", "users", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade
+ add_foreign_key "namespace_aggregation_schedules", "namespaces", on_delete: :cascade
+ add_foreign_key "namespace_root_storage_statistics", "namespaces", on_delete: :cascade
add_foreign_key "namespace_statistics", "namespaces", on_delete: :cascade
add_foreign_key "namespaces", "namespaces", column: "custom_project_templates_group_id", name: "fk_e7a0b20a6b", on_delete: :nullify
add_foreign_key "namespaces", "plans", name: "fk_fdd12e5b80", on_delete: :nullify
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index 0e655e49922..b1126881440 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -158,12 +158,11 @@ If you enable Monitoring, it must be enabled on **all** GitLab servers.
sidekiq['listen_address'] = "0.0.0.0"
unicorn['listen'] = '0.0.0.0'
- # Add the monitoring node's IP address to the monitoring whitelist and allow it to scrape the NGINX metrics
- # Replace placeholder
- # monitoring.gitlab.example.com
- # with the addresses gathered for the monitoring node
- gitlab_rails['monitoring_whitelist'] = ['monitoring.gitlab.example.com']
- nginx['status']['options']['allow'] = ['monitoring.gitlab.example.com']
+ # Add the monitoring node's IP address to the monitoring whitelist and allow it to
+ # scrape the NGINX metrics. Replace placeholder `monitoring.gitlab.example.com` with
+ # the address and/or subnets gathered from the monitoring node(s).
+ gitlab_rails['monitoring_whitelist'] = ['monitoring.gitlab.example.com', '127.0.0.0/8']
+ nginx['status']['options']['allow'] = ['monitoring.gitlab.example.com', '127.0.0.0/8']
```
1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
diff --git a/doc/administration/high_availability/pgbouncer.md b/doc/administration/high_availability/pgbouncer.md
index 762179cf756..053dae25823 100644
--- a/doc/administration/high_availability/pgbouncer.md
+++ b/doc/administration/high_availability/pgbouncer.md
@@ -62,6 +62,33 @@ See our [HA documentation for PostgreSQL](database.md) for information on runnin
1. At this point, your instance should connect to the database through pgbouncer. If you are having issues, see the [Troubleshooting](#troubleshooting) section
+## Enable Monitoring
+
+> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/3786) in GitLab 12.0.
+
+ If you enable Monitoring, it must be enabled on **all** pgbouncer servers.
+
+ 1. Create/edit `/etc/gitlab/gitlab.rb` and add the following configuration:
+
+ ```ruby
+ # Enable service discovery for Prometheus
+ consul['enable'] = true
+ consul['monitoring_service_discovery'] = true
+
+ # Replace placeholders
+ # Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z
+ # with the addresses of the Consul server nodes
+ consul['configuration'] = {
+ retry_join: %w(Y.Y.Y.Y consul1.gitlab.example.com Z.Z.Z.Z),
+ }
+
+ # Set the network addresses that the exporters will listen on
+ node_exporter['listen_address'] = '0.0.0.0:9100'
+ pgbouncer_exporter['listen_address'] = '0.0.0.0:9188'
+ ```
+
+ 1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
+
### Interacting with pgbouncer
#### Administrative console
diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md
index 286b99aceb5..7297507f599 100644
--- a/doc/administration/operations/extra_sidekiq_processes.md
+++ b/doc/administration/operations/extra_sidekiq_processes.md
@@ -1,70 +1,132 @@
# Extra Sidekiq processes **[STARTER ONLY]**
-GitLab Enterprise Edition allows one to start an extra set of Sidekiq processes
+NOTE: **Note:**
+The information in this page applies only to Omnibus GitLab.
+
+GitLab Starter allows one to start an extra set of Sidekiq processes
besides the default one. These processes can be used to consume a dedicated set
of queues. This can be used to ensure certain queues always have dedicated
workers, no matter the number of jobs that need to be processed.
-## Starting extra processes via Omnibus GitLab
+## Available Sidekiq queues
-To enable `sidekiq-cluster`, you must apply the `sidekiq_cluster['enable'] = true`
-setting `/etc/gitlab/gitlab.rb`:
+For a list of the existing Sidekiq queues, check the following files:
-```ruby
-sidekiq_cluster['enable'] = true
-```
+- [Queues for both GitLab Community and Enterprise Editions](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/app/workers/all_queues.yml)
+- [Queues for GitLab Enterprise Editions only](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/app/workers/all_queues.yml)
-You will then specify how many additional processes to create via `sidekiq-cluster`
-as well as which queues for them to handle. This is done via the
-`sidekiq_cluster['queue_groups']` setting. This is an array whose items contain
-which queues to process. Each item in the array will equate to one additional
-sidekiq process.
+Each entry in the above files represents a queue on which extra Sidekiq processes
+can be started.
-As an example, to make additional sidekiq processes that process the
-`elastic_indexer` and `mailers` queues, you would apply the following:
+## Starting extra processes
-```ruby
-sidekiq_cluster['queue_groups'] = [
- "elastic_indexer",
- "mailers"
-]
-```
+To start extra Sidekiq processes, you must enable `sidekiq-cluster`:
-To have an additional sidekiq process handle multiple queues, you simply put a
-comma after the first queue name and then put the next queue name:
+1. Edit `/etc/gitlab/gitlab.rb` and add:
-```ruby
-sidekiq_cluster['queue_groups'] = [
- "elastic_indexer,elastic_commit_indexer",
- "mailers"
-]
-```
+ ```ruby
+ sidekiq_cluster['enable'] = true
+ ```
-Keep in mind, all changes must be followed by reconfiguring your GitLab
-application via `sudo gitlab-ctl reconfigure`.
+1. You will then need to specify how many additional processes to create via `sidekiq-cluster`
+ and which queue they should handle via the `sidekiq_cluster['queue_groups']`
+ array setting. Each item in the array equates to one additional Sidekiq
+ process, and values in each item determine the queues it works on.
-### Monitoring
+ For example, the following setting adds additional Sidekiq processes to two
+ queues, one to `elastic_indexer` and one to `mailers`:
-Once the Sidekiq processes are added, you can visit the "Background Jobs"
+ ```ruby
+ sidekiq_cluster['queue_groups'] = [
+ "elastic_indexer",
+ "mailers"
+ ]
+ ```
+
+ To have an additional Sidekiq process handle multiple queues, add multiple
+ queue names to its item delimited by commas. For example:
+
+ ```ruby
+ sidekiq_cluster['queue_groups'] = [
+ "elastic_indexer, elastic_commit_indexer",
+ "mailers"
+ ]
+ ```
+
+1. Save the file and reconfigure GitLab for the changes to take effect:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
+
+Once the extra Sidekiq processes are added, you can visit the "Background Jobs"
section under the admin area in GitLab (`/admin/background_jobs`).
-![Extra sidekiq processes](img/sidekiq-cluster.png)
+![Extra Sidekiq processes](img/sidekiq-cluster.png)
-### All queues with exceptions
+## Negating settings
-To have the additional sidekiq processes work on every queue EXCEPT the ones
+To have the additional Sidekiq processes work on every queue **except** the ones
you list:
+1. After you follow the steps for [starting extra processes](#starting-extra-processes),
+ edit `/etc/gitlab/gitlab.rb` and add:
+
+ ```ruby
+ sidekiq_cluster['negate'] = true
+ ```
+
+1. Save the file and reconfigure GitLab for the changes to take effect:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
+
+## Ignore all GitHub import queues
+
+When [importing from GitHub](../../user/project/import/github.md), Sidekiq might
+use all of its resources to perform those operations. To set up a separate
+`sidekiq-cluster` process to ignore all GitHub import-related queues:
+
1. Edit `/etc/gitlab/gitlab.rb` and add:
```ruby
+ sidekiq_cluster['enable'] = true
sidekiq_cluster['negate'] = true
+ sidekiq_cluster['queue_groups'] = [
+ "github_import_advance_stage",
+ "github_importer:github_import_import_diff_note",
+ "github_importer:github_import_import_issue",
+ "github_importer:github_import_import_note",
+ "github_importer:github_import_import_lfs_object",
+ "github_importer:github_import_import_pull_request",
+ "github_importer:github_import_refresh_import_jid",
+ "github_importer:github_import_stage_finish_import",
+ "github_importer:github_import_stage_import_base_data",
+ "github_importer:github_import_stage_import_issues_and_diff_notes",
+ "github_importer:github_import_stage_import_notes",
+ "github_importer:github_import_stage_import_lfs_objects",
+ "github_importer:github_import_stage_import_pull_requests",
+ "github_importer:github_import_stage_import_repository"
+ ]
```
-1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+1. Save the file and reconfigure GitLab for the changes to take effect:
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
-### Limiting concurrency
+## Number of threads
+
+Each process defined under `sidekiq_cluster` starts with a
+number of threads that equals the number of queues, plus one spare thread.
+For example, a process that handles the `process_commit` and `post_receive`
+queues will use three threads in total.
+
+## Limiting concurrency
+
+To limit the concurrency of the Sidekiq processes:
1. Edit `/etc/gitlab/gitlab.rb` and add:
@@ -72,11 +134,22 @@ you list:
sidekiq_cluster['concurrency'] = 25
```
-1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
+1. Save the file and reconfigure GitLab for the changes to take effect:
-Keep in mind, this normally would not exceed the number of CPU cores available.
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
-### Modifying the check interval
+For each queue group, the concurrency factor will be set to `min(number of queues, N)`.
+Setting the value to 0 will disable the limit. Keep in mind this normally would
+not exceed the number of CPU cores available.
+
+Each thread requires a Redis connection, so adding threads may
+increase Redis latency and potentially cause client timeouts. See the [Sidekiq
+documentation about Redis](https://github.com/mperham/sidekiq/wiki/Using-Redis)
+for more details.
+
+## Modifying the check interval
To modify the check interval for the additional Sidekiq processes:
@@ -90,9 +163,14 @@ To modify the check interval for the additional Sidekiq processes:
This tells the additional processes how often to check for enqueued jobs.
-## Starting extra processes via command line
+## Troubleshooting using the CLI
-Starting extra Sidekiq processes can be done using the command
+CAUTION: **Warning:**
+It's recommended to use `/etc/gitlab/gitlab.rb` to configure the Sidekiq processes.
+If you experience a problem, you should contact GitLab support. Use the command
+line at your own risk.
+
+For debugging purposes, you can start extra Sidekiq processes by using the command
`/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster`. This command
takes arguments using the following syntax:
@@ -111,29 +189,29 @@ see the relevant section in the
[Sidekiq style guide](../../development/sidekiq_style_guide.md#queue-namespaces).
For example, say you want to start 2 extra processes: one to process the
-"process_commit" queue, and one to process the "post_receive" queue. This can be
+`process_commit` queue, and one to process the `post_receive` queue. This can be
done as follows:
```bash
/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster process_commit post_receive
```
-If you instead want to start one process processing both queues you'd use the
+If you instead want to start one process processing both queues, you'd use the
following syntax:
```bash
/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster process_commit,post_receive
```
-If you want to have one Sidekiq process process the "process_commit" and
-"post_receive" queues, and one process to process the "gitlab_shell" queue,
+If you want to have one Sidekiq process dealing with the `process_commit` and
+`post_receive` queues, and one process to process the `gitlab_shell` queue,
you'd use the following:
```bash
/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster process_commit,post_receive gitlab_shell
```
-### Monitoring
+### Monitoring the `sidekiq-cluster` command
The `sidekiq-cluster` command will not terminate once it has started the desired
amount of Sidekiq processes. Instead, the process will continue running and
@@ -172,24 +250,24 @@ command and not the PID(s) of the started Sidekiq processes.
The Rails environment can be set by passing the `--environment` flag to the
`sidekiq-cluster` command, or by setting `RAILS_ENV` to a non-empty value. The
-default value is "development".
+default value can be found in `/opt/gitlab/etc/gitlab-rails/env/RAILS_ENV`.
-### All queues with exceptions
+### Using negation
You're able to run all queues in `sidekiq_queues.yml` file on a single or
multiple processes with exceptions using the `--negate` flag.
For example, say you want to run a single process for all queues,
-except "process_commit" and "post_receive". You can do so by executing:
+except `process_commit` and `post_receive`:
```bash
-sidekiq-cluster process_commit,post_receive --negate
+/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster process_commit,post_receive --negate
```
-For multiple processes of all queues (except "process_commit" and "post_receive"):
+For multiple processes of all queues (except `process_commit` and `post_receive`):
```bash
-sidekiq-cluster process_commit,post_receive process_commit,post_receive --negate
+/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster process_commit,post_receive process_commit,post_receive --negate
```
### Limiting concurrency
@@ -201,18 +279,3 @@ the `-m N` option. For example, this would cap the maximum number of threads to
```bash
/opt/gitlab/embedded/service/gitlab-rails/ee/bin/sidekiq-cluster process_commit,post_receive -m 1
```
-
-For each queue group, the concurrency factor will be set to min(number of
-queues, N). Setting the value to 0 will disable the limit.
-
-Note that each thread requires a Redis connection, so adding threads may
-increase Redis latency and potentially cause client timeouts. See the [Sidekiq
-documentation about Redis](https://github.com/mperham/sidekiq/wiki/Using-Redis)
-for more details.
-
-## Number of threads
-
-Each process started using `sidekiq-cluster` (whether it be via command line or
-via the gitlab.rb file) starts with a number of threads that equals the number
-of queues, plus one spare thread. For example, a process that handles the
-"process_commit" and "post_receive" queues will use 3 threads in total.
diff --git a/doc/api/settings.md b/doc/api/settings.md
index c2a1f7feefd..b01cec64837 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -142,8 +142,6 @@ are listed in the descriptions of the relevant settings.
| `authorized_keys_enabled` | boolean | no | By default, we write to the `authorized_keys` file to support Git over SSH without additional configuration. GitLab can be optimized to authenticate SSH keys via the database file. Only disable this if you have configured your OpenSSH server to use the AuthorizedKeysCommand. |
| `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. |
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It will automatically build, test, and deploy applications based on a predefined CI/CD configuration. |
-| `clientside_sentry_dsn` | string | required by: `clientside_sentry_enabled` | Clientside Sentry Data Source Name. |
-| `clientside_sentry_enabled` | boolean | no | (**If enabled, requires:** `clientside_sentry_dsn`) Enable Sentry error reporting for the client side. |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes. |
| `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts. |
| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take: `0` _(not protected, both developers and maintainers can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and maintainers can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but maintainers can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. |
@@ -212,8 +210,6 @@ are listed in the descriptions of the relevant settings.
| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is `null` which means there is no restriction. |
| `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. |
| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up. |
-| `sentry_dsn` | string | required by: `sentry_enabled` | Sentry Data Source Name. |
-| `sentry_enabled` | boolean | no | (**If enabled, requires:** `sentry_dsn`) Sentry is an error reporting and logging tool which is currently not shipped with GitLab, available at <https://sentry.io>. |
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text`) Enable shared runners for new projects. |
| `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. |
@@ -231,6 +227,7 @@ are listed in the descriptions of the relevant settings.
| `throttle_unauthenticated_enabled` | boolean | no | (**If enabled, requires:** `throttle_unauthenticated_period_in_seconds` and `throttle_unauthenticated_requests_per_period`) Enable unauthenticated request rate limit. Helps reduce request volume (e.g. from crawlers or abusive bots). |
| `throttle_unauthenticated_period_in_seconds` | integer | required by: `throttle_unauthenticated_enabled` | Rate limit period in seconds. |
| `throttle_unauthenticated_requests_per_period` | integer | required by: `throttle_unauthenticated_enabled` | Max requests per period per IP. |
+| `time_tracking_limit_to_hours` | boolean | no | Limit display of time tracking units to hours. Default is `false`. |
| `two_factor_grace_period` | integer | required by: `require_two_factor_authentication` | Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication. |
| `unique_ips_limit_enabled` | boolean | no | (**If enabled, requires:** `unique_ips_limit_per_user` and `unique_ips_limit_time_window`) Limit sign in from multiple ips. |
| `unique_ips_limit_per_user` | integer | required by: `unique_ips_limit_enabled` | Maximum number of ips per user. |
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 1743c38eb46..da864a0b3cc 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -63,7 +63,7 @@ Once you're familiar with how GitLab CI/CD works, see the
for all the attributes you can set and use.
NOTE: **Note:**
-GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/admin_area/settings/continuous_integration.md#extra-shared-runners-pipeline-minutes-quota).
+GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/admin_area/settings/continuous_integration.md#extra-shared-runners-pipeline-minutes-quota-free-only).
## Configuration
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index b4c4bea6447..efdcaf5a6f5 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -205,7 +205,14 @@ An example project using this approach can be found here: <https://gitlab.com/gi
### Use Docker socket binding
-The third approach is to bind-mount `/var/run/docker.sock` into the container so that docker is available in the context of that image.
+The third approach is to bind-mount `/var/run/docker.sock` into the
+container so that Docker is available in the context of that image.
+
+NOTE: **Note:**
+If you bind the Docker socket [when using GitLab Runner 11.11 or
+newer](https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1261),
+you can no longer use `docker:dind` as a service because volume bindings
+are done to the services as well, making these incompatible.
In order to do that, follow the steps:
diff --git a/doc/development/README.md b/doc/development/README.md
index af3207671e6..5df6ec5fd56 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -108,6 +108,7 @@ description: 'Learn how to contribute to GitLab.'
- [Database Debugging and Troubleshooting](database_debugging.md)
- [Query Count Limits](query_count_limits.md)
- [Database helper modules](database_helpers.md)
+- [Code comments](code_comments.md)
## Integration guides
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 2ed2a905db7..aeddad14995 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -447,7 +447,7 @@ want to validate the abilities for.
Alternatively, we can add a `find_object` method that will load the
object on the mutation. This would allow you to use the
-`authorized_find!` and `authorized_find!` helper methods.
+`authorized_find!` helper method.
When a user is not allowed to perform the action, or an object is not
found, we should raise a
diff --git a/doc/development/code_comments.md b/doc/development/code_comments.md
new file mode 100644
index 00000000000..36962eb46d4
--- /dev/null
+++ b/doc/development/code_comments.md
@@ -0,0 +1,14 @@
+# Code comments
+
+Whenever you add comment to the code that is expected to be addressed at any time
+in future, please create a technical debt issue for it. Then put a link to it
+to the code comment you've created. This will allow other developers to quickly
+check if a comment is still relevant and what needs to be done to address it.
+
+Examples:
+
+```rb
+# Deprecated scope until code_owner column has been migrated to rule_type.
+# To be removed with https://gitlab.com/gitlab-org/gitlab-ee/issues/11834.
+scope :code_owner, -> { where(code_owner: true).or(where(rule_type: :code_owner)) }
+```
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 23d52a33881..ff6dc16d1a0 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -409,11 +409,20 @@ To indicate the steps of navigation through the UI:
## Images
- Place images in a separate directory named `img/` in the same directory where
- the `.md` document that you're working on is located. Always prepend their
- names with the name of the document that they will be included in. For
- example, if there is a document called `twitter.md`, then a valid image name
- could be `twitter_login_screen.png`.
-- Images should have a specific, non-generic name that will differentiate and describe them properly.
+ the `.md` document that you're working on is located.
+- Images should have a specific, non-generic name that will
+ differentiate and describe them properly.
+- Always add to the end of the file name the GitLab release version
+ number corresponding to the release milestone the image was added to,
+ or corresponding to the release the screenshot was taken from, using the
+ format `image_name_vX_Y.png`.
+ ([Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/61027) in GitLab 12.1.)
+- For example, for a screenshot taken from the pipelines page of
+ GitLab 11.1, a valid name is `pipelines_v11_1.png`. If you're
+ adding an illustration that does not include parts of the UI,
+ add the release number corresponding to the release the image
+ was added to. Example, for an MR added to 11.1's milestone,
+ a valid name for an illustration is `devops_diagram_v11_1.png`.
- Keep all file names in lower case.
- Consider using PNG images instead of JPEG.
- Compress all images with <https://tinypng.com/> or similar tool.
@@ -426,7 +435,7 @@ To indicate the steps of navigation through the UI:
Inside the document:
- The Markdown way of using an image inside a document is:
- `![Proper description what the image is about](img/document_image_title.png)`
+ `![Proper description what the image is about](img/document_image_title_vX_Y.png)`
- Always use a proper description for what the image is about. That way, when a
browser fails to show the image, this text will be used as an alternative
description.
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 4c9d1684c00..28ebb6f0f64 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -27,14 +27,30 @@ we need to solve before being able to use Jest for all our needs.
### Differences to Karma
- Jest runs in a Node.js environment, not in a browser. Support for running Jest tests in a browser [is planned](https://gitlab.com/gitlab-org/gitlab-ce/issues/58205).
-- Because Jest runs in a Node.js environment, it uses [jsdom](https://github.com/jsdom/jsdom) by default.
+- Because Jest runs in a Node.js environment, it uses [jsdom](https://github.com/jsdom/jsdom) by default. See also its [limitations](#limitations-of-jsdom) below.
+- Jest does not have access to Webpack loaders or aliases.
+ The aliases used by Jest are defined in its [own config](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/jest.config.js).
- All calls to `setTimeout` and `setInterval` are mocked away. See also [Jest Timer Mocks](https://jestjs.io/docs/en/timer-mocks).
- `rewire` is not required because Jest supports mocking modules. See also [Manual Mocks](https://jestjs.io/docs/en/manual-mocks).
+- No [context object](https://jasmine.github.io/tutorials/your_first_suite#section-The_%3Ccode%3Ethis%3C/code%3E_keyword) is passed to tests in Jest.
+ This means sharing `this.something` between `beforeEach()` and `it()` for example does not work.
+ Instead you should declare shared variables in the context that they are needed (via `const` / `let`).
- The following will cause tests to fail in Jest:
- Unmocked requests.
- Unhandled Promise rejections.
- Calls to `console.warn`, including warnings from libraries like Vue.
+### Limitations of jsdom
+
+As mentioned [above](#differences-to-karma), Jest uses jsdom instead of a browser for running tests.
+This comes with a number of limitations, namely:
+
+- [No scrolling support](https://github.com/jsdom/jsdom/blob/15.1.1/lib/jsdom/browser/Window.js#L623-L625)
+- [No element sizes or positions](https://github.com/jsdom/jsdom/blob/15.1.1/lib/jsdom/living/nodes/Element-impl.js#L334-L371)
+- [No layout engine](https://github.com/jsdom/jsdom/issues/1322) in general
+
+See also the issue for [support running Jest tests in browsers](https://gitlab.com/gitlab-org/gitlab-ce/issues/58205).
+
### Debugging Jest tests
Running `yarn jest-debug` will run Jest in debug mode, allowing you to debug/inspect as described in the [Jest docs](https://jestjs.io/docs/en/troubleshooting#tests-are-failing-and-you-don-t-know-why).
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 5768a97e727..84596ff6a2c 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -94,13 +94,11 @@ a group in the **Usage Quotas** page available to the group page settings list.
![Group pipelines quota](img/group_pipelines_quota.png)
-## Extra Shared Runners pipeline minutes quota
+## Extra Shared Runners pipeline minutes quota **[FREE ONLY]**
-NOTE: **Note:**
-Only available on GitLab.com.
-
-You can purchase additional CI minutes so your pipelines will not be blocked after you have
-used all your CI minutes from your main quota.
+If you're using GitLab.com, you can purchase additional CI minutes so your
+pipelines will not be blocked after you have used all your CI minutes from your
+main quota.
In order to purchase additional minutes, you should follow these steps:
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index dc86e66cb4f..ea8b96eb24d 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -46,13 +46,16 @@ this is enabled by default.
The following languages and dependency managers are supported.
-| Language (package managers) | Scan tool |
-|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
-| JavaScript ([npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/en/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general), [Retire.js](https://retirejs.github.io/retire.js) |
-| Python ([pip](https://pip.pypa.io/en/stable/)) (only `requirements.txt` supported) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) |
-| Ruby ([gem](https://rubygems.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general), [bundler-audit](https://github.com/rubysec/bundler-audit) |
-| Java ([Maven](https://maven.apache.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) |
-| PHP ([Composer](https://getcomposer.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium/general) |
+| Language (package managers) | Supported | Scan tool(s) |
+|----------------------------- | --------- | ------------ |
+| JavaScript ([npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/en/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [Retire.js](https://retirejs.github.io/retire.js) |
+| Python ([pip](https://pip.pypa.io/en/stable/)) (only `requirements.txt` supported) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
+| Ruby ([gem](https://rubygems.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bundler-audit](https://github.com/rubysec/bundler-audit) |
+| Java ([Maven](https://maven.apache.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
+| PHP ([Composer](https://getcomposer.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
+| Python ([poetry](https://poetry.eustace.io/)) | no ([issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/7006 "Support Poetry in Dependency Scanning")) | not available |
+| Python ([Pipfile](https://docs.pipenv.org/en/latest/basics/)) | no ([issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/11756 "Pipfile.lock support for Dependency Scanning"))| not available |
+| Go ([Golang](https://golang.org/)) | no ([issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/7132 "Dependency Scanning for Go")) | not available |
## Remote checks
diff --git a/doc/user/operations_dashboard/index.md b/doc/user/operations_dashboard/index.md
index 66362f27299..54bf3ff8a40 100644
--- a/doc/user/operations_dashboard/index.md
+++ b/doc/user/operations_dashboard/index.md
@@ -1,9 +1,6 @@
# Operations Dashboard **[PREMIUM]**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5781)
-in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.5.
-[Moved](https://gitlab.com/gitlab-org/gitlab-ee/issues/9218) to
-[GitLab Premium](https://about.gitlab.com/pricing/) in 11.10.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5781) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.5. [Moved](https://gitlab.com/gitlab-org/gitlab-ee/issues/9218) to [GitLab Premium](https://about.gitlab.com/pricing/) in 11.10.
The Operations Dashboard provides a summary of each project's operational health,
including pipeline and alert status.
@@ -16,9 +13,9 @@ dashboard icon:
## Adding a project to the dashboard
NOTE: **Note:**
-For GitLab.com, the Operations Dashboard is available for free for public projects.
-If your project is private, the group it belongs to must have a
-[Gold](https://about.gitlab.com/pricing/) plan.
+For GitLab.com, you can add your project to the Operations Dashboard for free if
+your project is public. If your project is private, the group it belongs to must
+have a [Silver](https://about.gitlab.com/pricing/) plan.
To add a project to the dashboard:
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index df413a11af0..26cacbe5545 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -84,9 +84,8 @@ Click on **Register U2F Device** to complete the process.
> **Note:**
Recovery codes are not generated for U2F devices.
-Should you ever lose access to your one time password authenticator, you can use one of the ten provided
-backup codes to login to your account. We suggest copying them, printing them, or downloading them using
-the **Download codes** button for storage in a safe place.
+Immediately after successfully enabling two-factor authentication, you'll be prompted to download a set of set recovery codes. Should you ever lose access to your one time password authenticator, you can use one of them to log in to your account. We suggest copying them, printing them, or downloading them using
+the **Download codes** button for storage in a safe place. If you choose to download them, the file will be called **gitlab-recovery-codes.txt**.
CAUTION: **Caution:**
Each code can be used only once to log in to your account.
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index a0fe97f2b9d..97d2dfc0f7e 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -519,8 +519,11 @@ service account of the cluster integration.
### Troubleshooting failed deployment jobs
-GitLab will create a namespace and service account specifically for your
-deployment jobs. This happens immediately before the deployment job starts.
+Before the deployment jobs starts, GitLab creates the following specifically for
+the deployment job:
+
+- A namespace.
+- A service account.
However, sometimes GitLab can not create them. In such instances, your job will fail with the message:
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 751e8e44e60..aab7131e353 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -160,7 +160,7 @@ receivers:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4925) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.11.
-Alerts can be used to trigger actions, like open an issue automatically. To configure the actions:
+Alerts can be used to trigger actions, like open an issue automatically (enabled by default since `12.1`). To configure the actions:
1. Navigate to your project's **Settings > Operations > Incidents**.
1. Enable the option to create issues.
diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
index c93c7a5fe08..0dd60d84c42 100644
--- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -42,6 +42,8 @@ Navigate to your project's settings page and expand the **Merge requests** secti
In the **Merge checks** subsection, select the **Pipelines must succeed** check
box and hit **Save** for the changes to take effect.
+NOTE: **Note:** This setting also prevents merge requests from being merged if there is no pipeline.
+
![Pipelines must succeed settings](img/merge_when_pipeline_succeeds_only_if_succeeds_settings.png)
From now on, every time the pipeline fails you will not be able to merge the
@@ -49,6 +51,21 @@ merge request from the UI, until you make all relevant jobs pass.
![Only allow merge if pipeline succeeds message](img/merge_when_pipeline_succeeds_only_if_succeeds_msg.png)
+### Limitations
+
+When this setting is enabled, a merge request is prevented from being merged if there is no pipeline. This may conflict with some use cases where [`only/except`](../../../ci/yaml/README.md#onlyexcept-advanced) rules are used and they don't generate any pipelines.
+
+Users that expect to be able to merge a merge request in this scenario should ensure that [there is always a pipeline](https://gitlab.com/gitlab-org/gitlab-ce/issues/54226) and that it's succesful.
+
+For example, to that on merge requests there is always a passing job even though `only/except` rules may not generate any other jobs:
+
+```yaml
+enable_merge:
+ only: merge_requests
+ script:
+ - echo true
+```
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index d585c19fc5c..bc9a11504cd 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -1,5 +1,5 @@
---
-last_updated: 2019-06-04
+last_updated: 2019-06-25
type: concepts, reference, howto
---
@@ -138,9 +138,9 @@ verify your domain's ownership with a TXT record:
> - **Do not** add any special chars after the default Pages
domain. E.g., **do not** point your `subdomain.domain.com` to
`namespace.gitlab.io.` or `namespace.gitlab.io/`.
-> - GitLab Pages IP on GitLab.com [was changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) in 2017
+> - GitLab Pages IP on GitLab.com [was changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) in 2017.
> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2018/07/19/gcp-move-update/#gitlab-pages-and-custom-domains)
- from `52.167.214.135` to `35.185.44.232` in 2018
+ from `52.167.214.135` to `35.185.44.232` in 2018.
### Add your custom domain to GitLab Pages settings
@@ -199,7 +199,7 @@ Certificates are NOT required to add to your custom
highly recommendable.
Let's start with an introduction to the importance of HTTPS.
-Alternatively, jump ahead to [adding certificates to your project](#adding-certificates-to-your-project).
+Alternatively, jump ahead to [adding certificates to your project](#adding-certificates-to-pages).
### Why should I care about HTTPS?
@@ -255,12 +255,12 @@ which also offers a [free CDN service](https://blog.cloudflare.com/cloudflares-f
Their certs are valid up to 15 years. See the tutorial on
[how to add a CloudFlare Certificate to your GitLab Pages website](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/).
-### Adding certificates to your project
+### Adding certificates to Pages
Regardless the CA you choose, the steps to add your certificate to
your Pages project are the same.
-### What do you need
+#### Requirements
1. A PEM certificate
1. An intermediate certificate
@@ -270,7 +270,7 @@ your Pages project are the same.
These fields are found under your **Project**'s **Settings** > **Pages** > **New Domain**.
-### What's what?
+#### Certificate types
- A PEM certificate is the certificate generated by the CA,
which needs to be added to the field **Certificate (PEM)**.
@@ -283,21 +283,32 @@ These fields are found under your **Project**'s **Settings** > **Pages** > **New
- A private key is an encrypted key which validates
your PEM against your domain.
-### Now what?
+#### Add the certificate to your project
-Now that you hopefully understand why you need all
-of this, it's simple:
+Once you've met the requirements:
-- Your PEM certificate needs to be added to the first field
+- Your PEM certificate needs to be added to the first field.
- If your certificate is missing its intermediate, copy
and paste the root certificate (usually available from your CA website)
and paste it in the [same field as your PEM certificate](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/),
just jumping a line between them.
-- Copy your private key and paste it in the last field
+- Copy your private key and paste it in the last field.
->**Note:**
+NOTE: **Note:**
**Do not** open certificates or encryption keys in
regular text editors. Always use code editors (such as
Sublime Text, Atom, Dreamweaver, Brackets, etc).
-_Read on about [Creating and Tweaking GitLab CI/CD for GitLab Pages](getting_started_part_four.md)_
+## Force HTTPS for GitLab Pages websites
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28857) in GitLab 10.7.
+
+To make your website's visitors even more secure, you can choose to
+force HTTPS for GitLab Pages. By doing so, all attempts to visit your
+website via HTTP will be automatically redirected to HTTPS via 301.
+
+It works with both GitLab's default domain and with your custom
+domain (as long as you've set a valid certificate for it).
+
+To enable this setting, navigate to your project's **Settings > Pages**
+and tick the checkbox **Force HTTPS (requires valid certificates)**.
diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md
index c03dffa967d..4286a3625a2 100644
--- a/doc/workflow/time_tracking.md
+++ b/doc/workflow/time_tracking.md
@@ -73,7 +73,15 @@ The following time units are available:
Default conversion rates are 1mo = 4w, 1w = 5d and 1d = 8h.
-Other interesting links:
+### Limit displayed units to hours
+
+> Introduced in GitLab 12.0.
+
+The display of time units can be limited to hours through the option in **Admin Area > Settings > Preferences** under 'Localization'.
+
+With this option enabled, `75h` is displayed instead of `1w 4d 3h`.
+
+## Other interesting links
- [Time Tracking landing page on about.gitlab.com](https://about.gitlab.com/solutions/time-tracking/)
diff --git a/lib/after_commit_queue.rb b/lib/after_commit_queue.rb
index 6fb7985f955..6a180fdf338 100644
--- a/lib/after_commit_queue.rb
+++ b/lib/after_commit_queue.rb
@@ -15,7 +15,7 @@ module AfterCommitQueue
end
def run_after_commit_or_now(&block)
- if AfterCommitQueue.inside_transaction?
+ if Gitlab::Database.inside_transaction?
if ActiveRecord::Base.connection.current_transaction.records.include?(self)
run_after_commit(&block)
else
@@ -32,18 +32,6 @@ module AfterCommitQueue
true
end
- def self.open_transactions_baseline
- if ::Rails.env.test?
- return DatabaseCleaner.connections.count { |conn| conn.strategy.is_a?(DatabaseCleaner::ActiveRecord::Transaction) }
- end
-
- 0
- end
-
- def self.inside_transaction?
- ActiveRecord::Base.connection.open_transactions > open_transactions_baseline
- end
-
protected
def _run_after_commit_queue
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 6767ef882cb..3c5c1a9fd5f 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -36,10 +36,6 @@ module API
given akismet_enabled: ->(val) { val } do
requires :akismet_api_key, type: String, desc: 'Generate API key at http://www.akismet.com'
end
- optional :clientside_sentry_enabled, type: Boolean, desc: 'Sentry can also be used for reporting and logging clientside exceptions. https://sentry.io/for/javascript/'
- given clientside_sentry_enabled: ->(val) { val } do
- requires :clientside_sentry_dsn, type: String, desc: 'Clientside Sentry Data Source Name'
- end
optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)'
optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts"
optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group'
@@ -114,10 +110,6 @@ module API
end
optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up'
- optional :sentry_enabled, type: Boolean, desc: 'Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: https://getsentry.com'
- given sentry_enabled: ->(val) { val } do
- requires :sentry_dsn, type: String, desc: 'Sentry Data Source Name'
- end
optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.'
optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects'
given shared_runners_enabled: ->(val) { val } do
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
index 46c4c755729..8a84744aa2d 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -17,7 +17,7 @@ code_quality:
--env SOURCE_CODE="$PWD"
--volume "$PWD":/code
--volume /var/run/docker.sock:/var/run/docker.sock
- "registry.gitlab.com/gitlab-org/security-products/codequality:11-8-stable" /code
+ "registry.gitlab.com/gitlab-org/security-products/codequality:12-0-stable" /code
artifacts:
reports:
codequality: gl-code-quality-report.json
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 8da98cc3909..e4d4779ba9a 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -234,6 +234,7 @@ module Gitlab
def self.connection
ActiveRecord::Base.connection
end
+ private_class_method :connection
def self.cached_column_exists?(table_name, column_name)
connection.schema_cache.columns_hash(table_name).has_key?(column_name.to_s)
@@ -243,8 +244,6 @@ module Gitlab
connection.schema_cache.data_source_exists?(table_name)
end
- private_class_method :connection
-
def self.database_version
row = connection.execute("SELECT VERSION()").first
@@ -272,5 +271,20 @@ module Gitlab
end
end
end
+
+ # inside_transaction? will return true if the caller is running within a transaction. Handles special cases
+ # when running inside a test environment, in which the entire test is running with a DatabaseCleaner transaction
+ def self.inside_transaction?
+ ActiveRecord::Base.connection.open_transactions > open_transactions_baseline
+ end
+
+ def self.open_transactions_baseline
+ if ::Rails.env.test?
+ return DatabaseCleaner.connections.count { |conn| conn.strategy.is_a?(DatabaseCleaner::ActiveRecord::Transaction) }
+ end
+
+ 0
+ end
+ private_class_method :open_transactions_baseline
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index a6739f12280..19b6aab1c4f 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -15,11 +15,6 @@ module Gitlab
SEARCH_CONTEXT_LINES = 3
REV_LIST_COMMIT_LIMIT = 2_000
- # In https://gitlab.com/gitlab-org/gitaly/merge_requests/698
- # We copied these two prefixes into gitaly-go, so don't change these
- # or things will break! (REBASE_WORKTREE_PREFIX and SQUASH_WORKTREE_PREFIX)
- REBASE_WORKTREE_PREFIX = 'rebase'.freeze
- SQUASH_WORKTREE_PREFIX = 'squash'.freeze
GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze
GITLAB_PROJECTS_TIMEOUT = Gitlab.config.gitlab_shell.git_timeout
EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'.freeze
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index d21b98d36ea..a80ce462ab0 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -271,26 +271,30 @@ module Gitlab
end
def find_commit(revision)
- if Gitlab::SafeRequestStore.active?
- # We don't use Gitlab::SafeRequestStore.fetch(key) { ... } directly
- # because `revision` can be a branch name, so we can't use it as a key
- # as it could point to another commit later on (happens a lot in
- # tests).
- key = {
- storage: @gitaly_repo.storage_name,
- relative_path: @gitaly_repo.relative_path,
- commit_id: revision
- }
- return Gitlab::SafeRequestStore[key] if Gitlab::SafeRequestStore.exist?(key)
-
- commit = call_find_commit(revision)
- return unless commit
-
- key[:commit_id] = commit.id unless GitalyClient.ref_name_caching_allowed?
+ return call_find_commit(revision) unless Gitlab::SafeRequestStore.active?
+
+ # We don't use Gitlab::SafeRequestStore.fetch(key) { ... } directly
+ # because `revision` can be a branch name, so we can't use it as a key
+ # as it could point to another commit later on (happens a lot in
+ # tests).
+ key = {
+ storage: @gitaly_repo.storage_name,
+ relative_path: @gitaly_repo.relative_path,
+ commit_id: revision
+ }
+ return Gitlab::SafeRequestStore[key] if Gitlab::SafeRequestStore.exist?(key)
+
+ commit = call_find_commit(revision)
+
+ if GitalyClient.ref_name_caching_allowed?
Gitlab::SafeRequestStore[key] = commit
- else
- call_find_commit(revision)
+ return commit
end
+
+ return unless commit
+
+ key[:commit_id] = commit.id
+ Gitlab::SafeRequestStore[key] = commit
end
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 582c3065189..92917028851 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -16,8 +16,8 @@ module Gitlab
gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
- if Gitlab::CurrentSettings.clientside_sentry_enabled
- gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn
+ if Gitlab.config.sentry.enabled
+ gon.sentry_dsn = Gitlab.config.sentry.clientside_dsn
gon.sentry_environment = Gitlab.config.sentry.environment
end
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb
index b367a97105c..ef5caaf5b0e 100644
--- a/lib/gitlab/graphql/authorize/authorize_resource.rb
+++ b/lib/gitlab/graphql/authorize/authorize_resource.rb
@@ -27,12 +27,6 @@ module Gitlab
raise NotImplementedError, "Implement #find_object in #{self.class.name}"
end
- def authorized_find(*args)
- object = find_object(*args)
-
- object if authorized?(object)
- end
-
def authorized_find!(*args)
object = find_object(*args)
authorize!(object)
@@ -48,6 +42,12 @@ module Gitlab
end
def authorized?(object)
+ # Sanity check. We don't want to accidentally allow a developer to authorize
+ # without first adding permissions to authorize against
+ if self.class.required_permissions.empty?
+ raise Gitlab::Graphql::Errors::ArgumentError, "#{self.class.name} has no authorizations"
+ end
+
self.class.required_permissions.all? do |ability|
# The actions could be performed across multiple objects. In which
# case the current user is common, and we could benefit from the
diff --git a/lib/gitlab/json_cache.rb b/lib/gitlab/json_cache.rb
index d01183d7845..84c6817f3c7 100644
--- a/lib/gitlab/json_cache.rb
+++ b/lib/gitlab/json_cache.rb
@@ -34,7 +34,7 @@ module Gitlab
def read(key, klass = nil)
value = backend.read(cache_key(key))
- value = parse_value(value, klass) if value
+ value = parse_value(value, klass) unless value.nil?
value
end
diff --git a/lib/gitlab/lets_encrypt.rb b/lib/gitlab/lets_encrypt.rb
new file mode 100644
index 00000000000..cdf24f24647
--- /dev/null
+++ b/lib/gitlab/lets_encrypt.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module LetsEncrypt
+ def self.enabled?(pages_domain = nil)
+ return false unless Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted
+
+ return false unless Feature.enabled?(:pages_auto_ssl)
+
+ # If no domain is passed, just check whether we're enabled globally
+ return true unless pages_domain
+
+ !!pages_domain.project && Feature.enabled?(:pages_auto_ssl_for_project, pages_domain.project)
+ end
+ end
+end
diff --git a/lib/gitlab/lets_encrypt/client.rb b/lib/gitlab/lets_encrypt/client.rb
index 66aea137012..ad2921ed555 100644
--- a/lib/gitlab/lets_encrypt/client.rb
+++ b/lib/gitlab/lets_encrypt/client.rb
@@ -34,14 +34,6 @@ module Gitlab
acme_client.terms_of_service
end
- def enabled?
- return false unless Feature.enabled?(:pages_auto_ssl)
-
- return false unless private_key
-
- Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted
- end
-
private
def acme_client
@@ -65,7 +57,7 @@ module Gitlab
end
def ensure_account
- raise 'Acme integration is disabled' unless enabled?
+ raise 'Acme integration is disabled' unless ::Gitlab::LetsEncrypt.enabled?
@acme_account ||= acme_client.new_account(contact: contact, terms_of_service_agreed: true)
end
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 72c44114001..764db14d720 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -4,7 +4,7 @@ module Gitlab
module Sentry
def self.enabled?
(Rails.env.production? || Rails.env.development?) &&
- Gitlab::CurrentSettings.sentry_enabled?
+ Gitlab.config.sentry.enabled
end
def self.context(current_user = nil)
diff --git a/lib/gitlab/time_tracking_formatter.rb b/lib/gitlab/time_tracking_formatter.rb
index cc206010e74..302da91328a 100644
--- a/lib/gitlab/time_tracking_formatter.rb
+++ b/lib/gitlab/time_tracking_formatter.rb
@@ -6,7 +6,7 @@ module Gitlab
def parse(string)
with_custom_config do
- string.sub!(/\A-/, '')
+ string = string.sub(/\A-/, '')
seconds = ChronicDuration.parse(string, default_unit: 'hours') rescue nil
seconds *= -1 if seconds && Regexp.last_match
@@ -16,10 +16,12 @@ module Gitlab
def output(seconds)
with_custom_config do
- ChronicDuration.output(seconds, format: :short, limit_to_hours: false, weeks: true) rescue nil
+ ChronicDuration.output(seconds, format: :short, limit_to_hours: limit_to_hours_setting, weeks: true) rescue nil
end
end
+ private
+
def with_custom_config
# We may want to configure it through project settings in a future version.
ChronicDuration.hours_per_day = 8
@@ -32,5 +34,9 @@ module Gitlab
result
end
+
+ def limit_to_hours_setting
+ Gitlab::CurrentSettings.time_tracking_limit_to_hours
+ end
end
end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index c531eb1d216..2bf71701b57 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -21,10 +21,10 @@ namespace :gitlab do
backup.cleanup
backup.remove_old
- puts "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \
+ progress.puts "Warning: Your gitlab.rb and gitlab-secrets.json files contain sensitive data \n" \
"and are not included in this backup. You will need these files to restore a backup.\n" \
"Please back them up manually.".color(:red)
- puts "Backup task is done."
+ progress.puts "Backup task is done."
end
# Restore backup of GitLab system
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 97613a6a920..2da0633f4cc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -41,6 +41,11 @@ msgid_plural "%d commits behind"
msgstr[0] ""
msgstr[1] ""
+msgid "%d commit,"
+msgid_plural "%d commits,"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d commits"
msgstr ""
@@ -391,6 +396,9 @@ msgstr ""
msgid "<no name set>"
msgstr ""
+msgid "<no scopes selected>"
+msgstr ""
+
msgid "<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes"
msgstr ""
@@ -502,6 +510,54 @@ msgstr ""
msgid "Access to '%{classification_label}' not allowed"
msgstr ""
+msgid "AccessTokens|Access Tokens"
+msgstr ""
+
+msgid "AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working."
+msgstr ""
+
+msgid "AccessTokens|Are you sure? Any issue email addresses currently in use will stop working."
+msgstr ""
+
+msgid "AccessTokens|Created"
+msgstr ""
+
+msgid "AccessTokens|Feed token"
+msgstr ""
+
+msgid "AccessTokens|Incoming email token"
+msgstr ""
+
+msgid "AccessTokens|It cannot be used to access any other data."
+msgstr ""
+
+msgid "AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens."
+msgstr ""
+
+msgid "AccessTokens|Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you. You should %{link_reset_it} if that ever happens."
+msgstr ""
+
+msgid "AccessTokens|Personal Access Tokens"
+msgstr ""
+
+msgid "AccessTokens|They are the only accepted password when you have Two-Factor Authentication (2FA) enabled."
+msgstr ""
+
+msgid "AccessTokens|You can also use personal access tokens to authenticate against Git over HTTP."
+msgstr ""
+
+msgid "AccessTokens|You can generate a personal access token for each application you use that needs access to the GitLab API."
+msgstr ""
+
+msgid "AccessTokens|Your feed token is used to authenticate you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar, and is included in those feed URLs."
+msgstr ""
+
+msgid "AccessTokens|Your incoming email token is used to authenticate you when you create a new issue by email, and is included in your personal project-specific email addresses."
+msgstr ""
+
+msgid "AccessTokens|reset it"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -514,6 +570,9 @@ msgstr ""
msgid "Active"
msgstr ""
+msgid "Active %{type} Tokens (%{token_length})"
+msgstr ""
+
msgid "Active Sessions"
msgstr ""
@@ -532,6 +591,9 @@ msgstr ""
msgid "Add README"
msgstr ""
+msgid "Add a %{type} token"
+msgstr ""
+
msgid "Add a GPG key"
msgstr ""
@@ -1159,6 +1221,9 @@ msgstr ""
msgid "Are you sure you want to reset the health check token?"
msgstr ""
+msgid "Are you sure you want to revoke this %{type} Token? This action cannot be undone."
+msgstr ""
+
msgid "Are you sure you want to revoke this nickname?"
msgstr ""
@@ -2064,6 +2129,9 @@ msgstr ""
msgid "Clear search input"
msgstr ""
+msgid "Clear templates search input"
+msgstr ""
+
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
@@ -2789,9 +2857,15 @@ msgstr ""
msgid "Configure Let's Encrypt"
msgstr ""
+msgid "Configure Prometheus"
+msgstr ""
+
msgid "Configure automatic git checks and housekeeping on repositories."
msgstr ""
+msgid "Configure existing installation"
+msgstr ""
+
msgid "Configure limits for web and API requests."
msgstr ""
@@ -2978,6 +3052,9 @@ msgstr ""
msgid "Copy link"
msgstr ""
+msgid "Copy personal access token to clipboard"
+msgstr ""
+
msgid "Copy reference to clipboard"
msgstr ""
@@ -3026,6 +3103,9 @@ msgstr ""
msgid "Create"
msgstr ""
+msgid "Create %{type} token"
+msgstr ""
+
msgid "Create New Directory"
msgstr ""
@@ -3146,6 +3226,9 @@ msgstr ""
msgid "Creates branch '%{branch_name}' and a merge request to resolve this issue"
msgstr ""
+msgid "Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available."
+msgstr ""
+
msgid "Cron Timezone"
msgstr ""
@@ -3517,6 +3600,9 @@ msgstr ""
msgid "Diff limits"
msgstr ""
+msgid "DiffsCompareBaseBranch|(base)"
+msgstr ""
+
msgid "Diffs|No file name available"
msgstr ""
@@ -3784,9 +3870,6 @@ msgstr ""
msgid "Enable HTML emails"
msgstr ""
-msgid "Enable Sentry for error reporting and logging."
-msgstr ""
-
msgid "Enable access to the Performance Bar for a given group."
msgstr ""
@@ -4006,9 +4089,6 @@ msgstr ""
msgid "Error"
msgstr ""
-msgid "Error Reporting and Logging"
-msgstr ""
-
msgid "Error Tracking"
msgstr ""
@@ -4231,6 +4311,12 @@ msgstr ""
msgid "Expired %{expiredOn}"
msgstr ""
+msgid "Expires"
+msgstr ""
+
+msgid "Expires at"
+msgstr ""
+
msgid "Expires in %{expires_at}"
msgstr ""
@@ -4656,6 +4742,9 @@ msgstr ""
msgid "Get started with error tracking"
msgstr ""
+msgid "Get started with performance monitoring"
+msgstr ""
+
msgid "Getting started with releases"
msgstr ""
@@ -5001,6 +5090,9 @@ msgstr ""
msgid "Help page text and support page url."
msgstr ""
+msgid "Helps prevent bots from creating accounts. We currently only support %{recaptcha_v2_link_start}reCAPTCHA v2%{recaptcha_v2_link_end}"
+msgstr ""
+
msgid "Hide archived projects"
msgstr ""
@@ -5333,6 +5425,9 @@ msgstr ""
msgid "Install a soft token authenticator like %{free_otp_link} or Google Authenticator from your application repository and scan this QR code. More information is available in the %{help_link_start}documentation%{help_link_end}."
msgstr ""
+msgid "Install on clusters"
+msgstr ""
+
msgid "Installed"
msgstr ""
@@ -5839,6 +5934,9 @@ msgstr ""
msgid "Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}."
msgstr ""
+msgid "Limit display of time tracking units to hours."
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] ""
@@ -5931,6 +6029,9 @@ msgstr ""
msgid "Make issue confidential."
msgstr ""
+msgid "Make sure you save it - you won't be able to access it again."
+msgstr ""
+
msgid "Make sure you're logged into the account that owns the projects you'd like to import."
msgstr ""
@@ -5979,6 +6080,9 @@ msgstr ""
msgid "Manual job"
msgstr ""
+msgid "ManualOrdering|Couldn't save the order of the issues"
+msgstr ""
+
msgid "Map a FogBugz account ID to a GitLab user"
msgstr ""
@@ -6584,6 +6688,9 @@ msgstr ""
msgid "No contributions were found"
msgstr ""
+msgid "No data found"
+msgstr ""
+
msgid "No details available"
msgstr ""
@@ -6647,6 +6754,9 @@ msgstr ""
msgid "No schedules"
msgstr ""
+msgid "No template"
+msgstr ""
+
msgid "No, directly import the existing email addresses and usernames."
msgstr ""
@@ -7051,6 +7161,9 @@ msgstr ""
msgid "Pick a name"
msgstr ""
+msgid "Pick a name for the application, and we'll give you a unique %{type} token."
+msgstr ""
+
msgid "Pin code"
msgstr ""
@@ -7576,6 +7689,9 @@ msgstr ""
msgid "Profiles|Full name"
msgstr ""
+msgid "Profiles|Impersonation"
+msgstr ""
+
msgid "Profiles|Include private contributions on my profile"
msgstr ""
@@ -7618,6 +7734,9 @@ msgstr ""
msgid "Profiles|Path"
msgstr ""
+msgid "Profiles|Personal Access"
+msgstr ""
+
msgid "Profiles|Position and size your new avatar"
msgstr ""
@@ -7753,6 +7872,12 @@ msgstr ""
msgid "Profiles|e.g. My MacBook key"
msgstr ""
+msgid "Profiles|impersonation"
+msgstr ""
+
+msgid "Profiles|personal access"
+msgstr ""
+
msgid "Profiles|username"
msgstr ""
@@ -8071,6 +8196,9 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "Prometheus server"
+msgstr ""
+
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
@@ -8541,6 +8669,9 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Reset template"
+msgstr ""
+
msgid "Resolve all discussions in new issue"
msgstr ""
@@ -8780,6 +8911,9 @@ msgstr ""
msgid "Scoped label"
msgstr ""
+msgid "Scopes"
+msgstr ""
+
msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right."
msgstr ""
@@ -9155,6 +9289,9 @@ msgstr ""
msgid "Show command"
msgstr ""
+msgid "Show comments"
+msgstr ""
+
msgid "Show comments only"
msgstr ""
@@ -9592,6 +9729,9 @@ msgstr ""
msgid "Status:"
msgstr ""
+msgid "Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
+msgstr ""
+
msgid "Stop environment"
msgstr ""
@@ -10491,6 +10631,9 @@ msgstr ""
msgid "This user cannot be unlocked manually from GitLab"
msgstr ""
+msgid "This user has no active %{type} Tokens."
+msgstr ""
+
msgid "This user has no identities"
msgstr ""
@@ -10503,6 +10646,9 @@ msgstr ""
msgid "Thursday"
msgstr ""
+msgid "Time"
+msgstr ""
+
msgid "Time based: Yes"
msgstr ""
@@ -10753,6 +10899,9 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
+msgid "To see all the user's personal access tokens you must impersonate them first."
+msgstr ""
+
msgid "To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there."
msgstr ""
@@ -10948,6 +11097,9 @@ msgstr ""
msgid "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
msgstr ""
+msgid "Unable to connect to Prometheus server"
+msgstr ""
+
msgid "Unable to connect to server: %{error}"
msgstr ""
@@ -11353,6 +11505,9 @@ msgstr ""
msgid "View details: %{details_url}"
msgstr ""
+msgid "View documentation"
+msgstr ""
+
msgid "View file @ "
msgstr ""
@@ -11428,6 +11583,9 @@ msgstr ""
msgid "Wait for the source to load to copy it to the clipboard"
msgstr ""
+msgid "Waiting for performance data"
+msgstr ""
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
@@ -11685,6 +11843,9 @@ msgstr ""
msgid "You are attempting to update a file that has changed since you started editing it."
msgstr ""
+msgid "You are connected to the Prometheus server, but there is currently no data to display."
+msgstr ""
+
msgid "You are going to remove %{group_name}, this will also remove all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
@@ -11964,6 +12125,9 @@ msgstr ""
msgid "Your Groups"
msgstr ""
+msgid "Your New Personal Access Token"
+msgstr ""
+
msgid "Your Primary Email will be used for avatar detection."
msgstr ""
@@ -12630,6 +12794,9 @@ msgstr ""
msgid "verify ownership"
msgstr ""
+msgid "version %{versionIndex}"
+msgstr ""
+
msgid "via %{closed_via}"
msgstr ""
diff --git a/package.json b/package.json
index 8e97419bfcb..577967d23b9 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
"apollo-upload-client": "^10.0.0",
"at.js": "^1.5.4",
"autosize": "^4.0.0",
- "axios": "^0.17.1",
+ "axios": "^0.19.0",
"babel-loader": "^8.0.5",
"bootstrap": "4.3.1",
"brace-expansion": "^1.1.8",
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
index f6f0468e76e..796de44a012 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module QA
- # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/49
- context 'Create', :smoke, :quarantine do
+ context 'Create', :smoke do
describe 'Snippet creation' do
it 'User creates a snippet' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
@@ -13,7 +12,7 @@ module QA
Resource::Snippet.fabricate_via_browser_ui! do |snippet|
snippet.title = 'Snippet title'
snippet.description = 'Snippet description'
- snippet.visibility = 'Public'
+ snippet.visibility = 'Private'
snippet.file_name = 'New snippet file name'
snippet.file_content = 'Snippet file text'
end
@@ -21,8 +20,7 @@ module QA
Page::Dashboard::Snippet::Show.perform do |snippet|
expect(snippet).to have_snippet_title('Snippet title')
expect(snippet).to have_snippet_description('Snippet description')
- expect(snippet).to have_embed_type('Embed')
- expect(snippet).to have_visibility_type('Public')
+ expect(snippet).to have_visibility_type('Private')
expect(snippet).to have_file_name('New snippet file name')
expect(snippet).to have_file_content('Snippet file text')
end
diff --git a/scripts/generate-gems-memory-metrics-static b/scripts/generate-gems-memory-metrics-static
new file mode 100755
index 00000000000..aa7ce3615bf
--- /dev/null
+++ b/scripts/generate-gems-memory-metrics-static
@@ -0,0 +1,18 @@
+#!/usr/bin/env ruby
+
+abort "usage: #{__FILE__} <memory_bundle_objects_file_name>" unless ARGV.length == 1
+memory_bundle_objects_file_name = ARGV.first
+
+full_report = File.readlines(memory_bundle_objects_file_name)
+
+allocated_str = full_report[1]
+retained_str = full_report[2]
+allocated_stats = /Total allocated: (?<bytes>.*) bytes \((?<objects>.*) objects\)/.match(allocated_str)
+retained_stats = /Total retained: (?<bytes>.*) bytes \((?<objects>.*) objects\)/.match(retained_str)
+
+abort 'failed to process the benchmark output' unless allocated_stats && retained_stats
+
+puts "memory_static_objects_allocated_mb #{(allocated_stats[:bytes].to_f / (1024 * 1024)).round(1)}"
+puts "memory_static_objects_retained_mb #{(retained_stats[:bytes].to_f / (1024 * 1024)).round(1)}"
+puts "memory_static_objects_allocated_items #{allocated_stats[:objects]}"
+puts "memory_static_objects_retained_items #{retained_stats[:objects]}"
diff --git a/scripts/generate-gems-size-metrics-static b/scripts/generate-gems-size-metrics-static
new file mode 100755
index 00000000000..ceec8aaccf1
--- /dev/null
+++ b/scripts/generate-gems-size-metrics-static
@@ -0,0 +1,30 @@
+#!/usr/bin/env ruby
+
+abort "usage: #{__FILE__} <memory_bundle_mem_file_name>" unless ARGV.length == 1
+memory_bundle_mem_file_name = ARGV.first
+
+full_report = File.readlines(memory_bundle_mem_file_name)
+
+def total_size(memory_bundle_mem_report)
+ stats = /TOP: (?<total_mibs_str>.*) MiB/.match(memory_bundle_mem_report.first)
+ abort 'failed to process the benchmark output' unless stats
+ "gem_total_size_mb #{stats[:total_mibs_str].to_f.round(1)}"
+end
+
+TOP_LEVEL_GEM_LOG_FORMAT = /^ (?<gem_name>\S.*):\s*(?<gem_size>\d[.\d]*)\s*MiB/.freeze
+def all_gems(memory_bundle_mem_report)
+ memory_bundle_mem_report.map do |line|
+ TOP_LEVEL_GEM_LOG_FORMAT.match(line)
+ end.compact
+end
+
+def gems_as_metrics(gems_match_data)
+ gems_match_data.map do |gem|
+ gem_name = gem[:gem_name]
+ gem_size_mb = gem[:gem_size].to_f.round(1)
+ "gem_size_mb{name=\"#{gem_name}\"} #{gem_size_mb}"
+ end
+end
+
+puts total_size(full_report)
+puts gems_as_metrics(all_gems(full_report)).sort(&:casecmp)
diff --git a/scripts/memory-static b/scripts/memory-static
deleted file mode 100755
index 54f147a7a91..00000000000
--- a/scripts/memory-static
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env ruby
-
-require_relative '../lib/gitlab/popen'
-
-full_report_filename, metrics_filename = ARGV
-abort 'usage: memory-static <full_report_filename> <metrics_filename>' unless full_report_filename && metrics_filename
-
-full_report, status = Gitlab::Popen.popen(%w(bundle exec derailed bundle:mem))
-abort 'failed to execute the benchmark' unless status.zero?
-
-File.open(full_report_filename, 'w') do |f|
- f.write(full_report)
-end
-
-stats = /TOP: (?<total_mibs_str>.*) MiB/.match(full_report.lines.first)
-abort 'failed to process the benchmark output' unless stats
-
-File.open(metrics_filename, 'a') do |f|
- f.puts "memory_static_total_mb #{stats[:total_mibs_str].to_f.round(1)}"
-end
diff --git a/scripts/memory-static-objects b/scripts/memory-static-objects
deleted file mode 100755
index 2ad38d9717c..00000000000
--- a/scripts/memory-static-objects
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env ruby
-
-require_relative '../lib/gitlab/popen'
-
-full_report_filename, metrics_filename = ARGV
-abort 'usage: memory-static-objects <full_report_filename> <metrics_filename>' unless full_report_filename && metrics_filename
-
-full_report, status = Gitlab::Popen.popen(%w(bundle exec derailed bundle:objects))
-abort 'failed to execute the benchmark' unless status.zero?
-
-File.open(full_report_filename, 'w') do |f|
- f.write(full_report)
-end
-
-allocated_str = full_report.lines[1]
-retained_str = full_report.lines[2]
-allocated_stats = /Total allocated: (?<bytes>.*) bytes \((?<objects>.*) objects\)/.match(allocated_str)
-retained_stats = /Total retained: (?<bytes>.*) bytes \((?<objects>.*) objects\)/.match(retained_str)
-
-abort 'failed to process the benchmark output' unless allocated_stats && retained_stats
-
-File.open(metrics_filename, 'a') do |f|
- f.puts "memory_static_objects_allocated_mb #{(allocated_stats[:bytes].to_f / (1024 * 1024)).round(1)}"
- f.puts "memory_static_objects_retained_mb #{(retained_stats[:bytes].to_f / (104 * 1024)).round(1)}"
- f.puts "memory_static_objects_allocated_items #{allocated_stats[:objects]}"
- f.puts "memory_static_objects_retained_items #{retained_stats[:objects]}"
-end
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 3bae2e08a6f..633ea28e96c 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -1,7 +1,7 @@
[[ "$TRACE" ]] && set -x
export TILLER_NAMESPACE="$KUBE_NAMESPACE"
-function deployExists() {
+function deploy_exists() {
local namespace="${1}"
local deploy="${2}"
echoinfo "Checking if ${deploy} exists in the ${namespace} namespace..." true
@@ -13,8 +13,7 @@ function deployExists() {
return $deploy_exists
}
-function previousDeployFailed() {
- set +e
+function previous_deploy_failed() {
local deploy="${1}"
echoinfo "Checking for previous deployment of ${deploy}" true
@@ -34,7 +33,6 @@ function previousDeployFailed() {
else
echoerr "Previous deployment NOT found."
fi
- set -e
return $status
}
@@ -51,49 +49,35 @@ function delete() {
helm delete --purge "$name"
}
-function cleanup() {
- if [ -z "$CI_ENVIRONMENT_SLUG" ]; then
- echoerr "No release given, aborting the delete!"
- return
- fi
-
- echoinfo "Cleaning up '$CI_ENVIRONMENT_SLUG'..." true
-
- kubectl -n "$KUBE_NAMESPACE" delete \
- ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa \
- --now --ignore-not-found --include-uninitialized \
- -l release="$CI_ENVIRONMENT_SLUG"
-}
-
function get_pod() {
local app_name="${1}"
local status="${2-Running}"
get_pod_cmd="kubectl get pods -n ${KUBE_NAMESPACE} --field-selector=status.phase=${status} -lapp=${app_name},release=${CI_ENVIRONMENT_SLUG} --no-headers -o=custom-columns=NAME:.metadata.name"
- echoinfo "Running '${get_pod_cmd}'" true
+ echoinfo "Waiting till '${app_name}' pod is ready" true
+ echoinfo "Running '${get_pod_cmd}'"
+ local interval=5
+ local elapsed_seconds=0
+ local max_seconds=$((2 * 60))
while true; do
local pod_name
pod_name="$(eval "${get_pod_cmd}")"
[[ "${pod_name}" == "" ]] || break
- echoinfo "Waiting till '${app_name}' pod is ready";
- sleep 5;
+ if [[ "${elapsed_seconds}" -gt "${max_seconds}" ]]; then
+ echoerr "The pod name couldn't be found after ${elapsed_seconds} seconds, aborting."
+ break
+ fi
+
+ printf "."
+ let "elapsed_seconds+=interval"
+ sleep ${interval}
done
echoinfo "The pod name is '${pod_name}'."
echo "${pod_name}"
}
-function perform_review_app_deployment() {
- check_kube_domain
- ensure_namespace
- install_tiller
- install_external_dns
- time deploy
- wait_for_review_app_to_be_accessible
- add_license
-}
-
function check_kube_domain() {
echoinfo "Checking that Kube domain exists..." true
@@ -119,9 +103,16 @@ function install_tiller() {
echoinfo "Initiating the Helm client..."
helm init --client-only
+ # Set toleration for Tiller to be installed on a specific node pool
helm init \
+ --wait \
--upgrade \
- --replicas 2
+ --node-selectors "app=helm" \
+ --replicas 3 \
+ --override "spec.template.spec.tolerations[0].key"="dedicated" \
+ --override "spec.template.spec.tolerations[0].operator"="Equal" \
+ --override "spec.template.spec.tolerations[0].value"="helm" \
+ --override "spec.template.spec.tolerations[0].effect"="NoSchedule"
kubectl rollout status -n "$TILLER_NAMESPACE" -w "deployment/tiller-deploy"
@@ -137,7 +128,7 @@ function install_external_dns() {
domain=$(echo "${REVIEW_APPS_DOMAIN}" | awk -F. '{printf "%s.%s", $(NF-1), $NF}')
echoinfo "Installing external DNS for domain ${domain}..." true
- if ! deployExists "${KUBE_NAMESPACE}" "${release_name}" || previousDeployFailed "${release_name}" ; then
+ if ! deploy_exists "${KUBE_NAMESPACE}" "${release_name}" || previous_deploy_failed "${release_name}" ; then
echoinfo "Installing external-dns Helm chart"
helm repo update
helm install stable/external-dns \
@@ -156,7 +147,7 @@ function install_external_dns() {
fi
}
-function create_secret() {
+function create_application_secret() {
echoinfo "Creating the ${CI_ENVIRONMENT_SLUG}-gitlab-initial-root-password secret in the ${KUBE_NAMESPACE} namespace..." true
kubectl create secret generic -n "$KUBE_NAMESPACE" \
@@ -165,7 +156,7 @@ function create_secret() {
--dry-run -o json | kubectl apply -f -
}
-function download_gitlab_chart() {
+function download_chart() {
echoinfo "Downloading the GitLab chart..." true
curl -o gitlab.tar.bz2 "https://gitlab.com/charts/gitlab/-/archive/${GITLAB_HELM_CHART_REF}/gitlab-${GITLAB_HELM_CHART_REF}.tar.bz2"
@@ -194,14 +185,12 @@ function deploy() {
gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-${IMAGE_VERSION}"
# Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade`
- if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previousDeployFailed "$CI_ENVIRONMENT_SLUG" ; then
+ if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previous_deploy_failed "$CI_ENVIRONMENT_SLUG" ; then
echo "Deployment in bad state, cleaning up $CI_ENVIRONMENT_SLUG"
delete
- cleanup
fi
- create_secret
- download_gitlab_chart
+ create_application_secret
HELM_CMD=$(cat << EOF
helm upgrade --install \
@@ -216,7 +205,7 @@ HELM_CMD=$(cat << EOF
--set prometheus.install=false \
--set global.ingress.configureCertmanager=false \
--set global.ingress.tls.secretName=tls-cert \
- --set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10"
+ --set global.ingress.annotations."external-dns\.alpha\.kubernetes\.io/ttl"="10" \
--set nginx-ingress.controller.service.enableHttp=false \
--set nginx-ingress.defaultBackend.resources.requests.memory=7Mi \
--set nginx-ingress.controller.resources.requests.memory=440M \
@@ -252,14 +241,35 @@ EOF
echoinfo "Deploying with:"
echoinfo "${HELM_CMD}"
- eval $HELM_CMD || true
+ eval "${HELM_CMD}"
+}
+
+function display_deployment_debug() {
+ migrations_pod=$(get_pod "migrations");
+ if [ -z "${migrations_pod}" ]; then
+ echoerr "Migrations pod not found."
+ else
+ echoinfo "Logs tail of the ${migrations_pod} pod..."
+
+ kubectl logs -n "$KUBE_NAMESPACE" "${migrations_pod}" | sed "s/${REVIEW_APPS_ROOT_PASSWORD}/[REDACTED]/g"
+ fi
+
+ unicorn_pod=$(get_pod "unicorn");
+ if [ -z "${unicorn_pod}" ]; then
+ echoerr "Unicorn pod not found."
+ else
+ echoinfo "Logs tail of the ${unicorn_pod} pod..."
+
+ kubectl logs -n "$KUBE_NAMESPACE" -c unicorn "${unicorn_pod}" | sed "s/${REVIEW_APPS_ROOT_PASSWORD}/[REDACTED]/g"
+ fi
}
function wait_for_review_app_to_be_accessible() {
- # In case the Review App isn't completely available yet. Keep trying for 5 minutes.
+ echoinfo "Waiting for the Review App at ${CI_ENVIRONMENT_URL} to be accessible..." true
+
local interval=5
local elapsed_seconds=0
- local max_seconds=$((5 * 60))
+ local max_seconds=$((2 * 60))
while true; do
local review_app_http_code
review_app_http_code=$(curl --silent --output /dev/null --max-time 5 --write-out "%{http_code}" "${CI_ENVIRONMENT_URL}/users/sign_in")
@@ -272,10 +282,10 @@ function wait_for_review_app_to_be_accessible() {
sleep ${interval}
done
- if [[ "${review_app_http_code}" == "200" ]]; then
- echoinfo "The Review App at ${CI_ENVIRONMENT_URL} is ready!"
+ if [[ "${review_app_http_code}" -eq "200" ]]; then
+ echoinfo "The Review App at ${CI_ENVIRONMENT_URL} is ready after ${elapsed_seconds} seconds!"
else
- echoerr "The Review App at ${CI_ENVIRONMENT_URL} isn't ready after 5 minutes of polling..."
+ echoerr "The Review App at ${CI_ENVIRONMENT_URL} isn't ready after ${max_seconds} seconds of polling..."
exit 1
fi
}
diff --git a/scripts/trigger-build b/scripts/trigger-build
index 52bc61cac56..4d8110fce10 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -122,7 +122,14 @@ module Trigger
end
def ref
- ENV['CNG_BRANCH'] || 'master'
+ default_ref =
+ if ENV['CI_COMMIT_REF_NAME'] =~ /^[\d-]+-stable(-ee)?$/
+ ENV['CI_COMMIT_REF_NAME']
+ else
+ 'master'
+ end
+
+ ENV['CNG_BRANCH'] || default_ref
end
def trigger_token
diff --git a/spec/controllers/concerns/continue_params_spec.rb b/spec/controllers/concerns/continue_params_spec.rb
index 5e47f5e9f28..b4b62cbe1e3 100644
--- a/spec/controllers/concerns/continue_params_spec.rb
+++ b/spec/controllers/concerns/continue_params_spec.rb
@@ -18,6 +18,14 @@ describe ContinueParams do
ActionController::Parameters.new(continue: params)
end
+ it 'returns an empty hash if params are not present' do
+ allow(controller).to receive(:params) do
+ ActionController::Parameters.new
+ end
+
+ expect(controller.continue_params).to eq({})
+ end
+
it 'cleans up any params that are not allowed' do
allow(controller).to receive(:params) do
strong_continue_params(to: '/hello',
diff --git a/spec/controllers/concerns/internal_redirect_spec.rb b/spec/controllers/concerns/internal_redirect_spec.rb
index 97119438ca1..da68c8c8697 100644
--- a/spec/controllers/concerns/internal_redirect_spec.rb
+++ b/spec/controllers/concerns/internal_redirect_spec.rb
@@ -15,44 +15,71 @@ describe InternalRedirect do
subject(:controller) { controller_class.new }
describe '#safe_redirect_path' do
- it 'is `nil` for invalid uris' do
- expect(controller.safe_redirect_path('Hello world')).to be_nil
+ where(:input) do
+ [
+ 'Hello world',
+ '//example.com/hello/world',
+ 'https://example.com/hello/world'
+ ]
end
- it 'is `nil` for paths trying to include a host' do
- expect(controller.safe_redirect_path('//example.com/hello/world')).to be_nil
+ with_them 'being invalid' do
+ it 'returns nil' do
+ expect(controller.safe_redirect_path(input)).to be_nil
+ end
end
- it 'returns the path if it is valid' do
- expect(controller.safe_redirect_path('/hello/world')).to eq('/hello/world')
+ where(:input) do
+ [
+ '/hello/world',
+ '/-/ide/project/path'
+ ]
end
- it 'returns the path with querystring if it is valid' do
- expect(controller.safe_redirect_path('/hello/world?hello=world#L123'))
- .to eq('/hello/world?hello=world#L123')
+ with_them 'being valid' do
+ it 'returns the path' do
+ expect(controller.safe_redirect_path(input)).to eq(input)
+ end
+
+ it 'returns the path with querystring and fragment' do
+ expect(controller.safe_redirect_path("#{input}?hello=world#L123"))
+ .to eq("#{input}?hello=world#L123")
+ end
end
end
describe '#safe_redirect_path_for_url' do
- it 'is `nil` for invalid urls' do
- expect(controller.safe_redirect_path_for_url('Hello world')).to be_nil
+ where(:input) do
+ [
+ 'Hello world',
+ 'http://example.com/hello/world',
+ 'http://test.host:3000/hello/world'
+ ]
end
- it 'is `nil` for urls from a with a different host' do
- expect(controller.safe_redirect_path_for_url('http://example.com/hello/world')).to be_nil
+ with_them 'being invalid' do
+ it 'returns nil' do
+ expect(controller.safe_redirect_path_for_url(input)).to be_nil
+ end
end
- it 'is `nil` for urls from a with a different port' do
- expect(controller.safe_redirect_path_for_url('http://test.host:3000/hello/world')).to be_nil
+ where(:input) do
+ [
+ 'http://test.host/hello/world'
+ ]
end
- it 'returns the path if the url is on the same host' do
- expect(controller.safe_redirect_path_for_url('http://test.host/hello/world')).to eq('/hello/world')
- end
+ with_them 'being on the same host' do
+ let(:path) { URI(input).path }
- it 'returns the path including querystring if the url is on the same host' do
- expect(controller.safe_redirect_path_for_url('http://test.host/hello/world?hello=world#L123'))
- .to eq('/hello/world?hello=world#L123')
+ it 'returns the path' do
+ expect(controller.safe_redirect_path_for_url(input)).to eq(path)
+ end
+
+ it 'returns the path with querystring and fragment' do
+ expect(controller.safe_redirect_path_for_url("#{input}?hello=world#L123"))
+ .to eq("#{path}?hello=world#L123")
+ end
end
end
@@ -82,12 +109,16 @@ describe InternalRedirect do
end
describe '#host_allowed?' do
- it 'allows uris with the same host and port' do
+ it 'allows URI with the same host and port' do
expect(controller.host_allowed?(URI('http://test.host/test'))).to be(true)
end
- it 'rejects uris with other host and port' do
+ it 'rejects URI with other host' do
expect(controller.host_allowed?(URI('http://example.com/test'))).to be(false)
end
+
+ it 'rejects URI with other port' do
+ expect(controller.host_allowed?(URI('http://test.host:3000/test'))).to be(false)
+ end
end
end
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index 3423fdf4c41..5ac5279e997 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -115,24 +115,34 @@ describe Projects::ForksController do
end
describe 'POST create' do
- def post_create
+ def post_create(params = {})
post :create,
params: {
namespace_id: project.namespace,
project_id: project,
namespace_key: user.namespace.id
- }
+ }.merge(params)
end
context 'when user is signed in' do
- it 'responds with status 302' do
+ before do
sign_in(user)
+ end
+ it 'responds with status 302' do
post_create
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project))
end
+
+ it 'passes continue params to the redirect' do
+ continue_params = { to: '/-/ide/project/path', notice: 'message' }
+ post_create continue: continue_params
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(response).to redirect_to(namespace_project_import_path(user.namespace, project, continue: continue_params))
+ end
end
context 'when user is not signed in' do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 9a598790ff2..faf3c990cb2 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -6,7 +6,8 @@ describe RegistrationsController do
include TermsHelper
describe '#create' do
- let(:user_params) { { user: { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } }
+ let(:base_user_params) { { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } }
+ let(:user_params) { { user: base_user_params } }
context 'email confirmation' do
around do |example|
@@ -105,6 +106,20 @@ describe RegistrationsController do
expect(subject.current_user.terms_accepted?).to be(true)
end
end
+
+ it "logs a 'User Created' message" do
+ stub_feature_flags(registrations_recaptcha: false)
+
+ expect(Gitlab::AppLogger).to receive(:info).with(/\AUser Created: username=new_username email=new@user.com.+\z/).and_call_original
+
+ post(:create, params: user_params)
+ end
+
+ it 'handles when params are new_user' do
+ post(:create, params: { new_user: base_user_params })
+
+ expect(subject.current_user).not_to be_nil
+ end
end
describe '#destroy' do
diff --git a/spec/factories/namespace/aggregation_schedules.rb b/spec/factories/namespace/aggregation_schedules.rb
new file mode 100644
index 00000000000..c172c3360e2
--- /dev/null
+++ b/spec/factories/namespace/aggregation_schedules.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :namespace_aggregation_schedules, class: Namespace::AggregationSchedule do
+ namespace
+ end
+end
diff --git a/spec/factories/namespace/root_storage_statistics.rb b/spec/factories/namespace/root_storage_statistics.rb
new file mode 100644
index 00000000000..54c5921eb44
--- /dev/null
+++ b/spec/factories/namespace/root_storage_statistics.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :namespace_root_storage_statistics, class: Namespace::RootStorageStatistics do
+ namespace
+ end
+end
diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb
index 6feafa5ece9..0cfc6e3aa46 100644
--- a/spec/factories/namespaces.rb
+++ b/spec/factories/namespaces.rb
@@ -19,5 +19,13 @@ FactoryBot.define do
owner.namespace = namespace
end
end
+
+ trait :with_aggregation_schedule do
+ association :aggregation_schedule, factory: :namespace_aggregation_schedules
+ end
+
+ trait :with_root_storage_statistics do
+ association :root_storage_statistics, factory: :namespace_root_storage_statistics
+ end
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index b1798c11361..6c9ae343e01 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -16,7 +16,9 @@ describe 'Issue Boards', :js do
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
- let(:card) { find('.board:nth-child(2)').first('.board-card') }
+ let(:card) { find('.board:nth-child(2)').first('.board-card') }
+
+ let(:application_settings) { {} }
around do |example|
Timecop.freeze { example.run }
@@ -27,6 +29,8 @@ describe 'Issue Boards', :js do
sign_in(user)
+ stub_application_setting(application_settings)
+
visit project_board_path(project, board)
wait_for_requests
end
@@ -223,16 +227,24 @@ describe 'Issue Boards', :js do
end
context 'time tracking' do
+ let(:compare_meter_tooltip) { find('.time-tracking .time-tracking-content .compare-meter')['data-original-title'] }
+
before do
issue2.timelogs.create(time_spent: 14400, user: user)
- issue2.update!(time_estimate: 28800)
+ issue2.update!(time_estimate: 128800)
+
+ click_card(card)
end
it 'shows time tracking progress bar' do
- click_card(card)
+ expect(compare_meter_tooltip).to eq('Time remaining: 3d 7h 46m')
+ end
+
+ context 'when time_tracking_limit_to_hours is true' do
+ let(:application_settings) { { time_tracking_limit_to_hours: true } }
- page.within('.time-tracking') do
- expect(find('.time-tracking-content .compare-meter')['data-original-title']).to eq('Time remaining: 4h')
+ it 'shows time tracking progress bar' do
+ expect(compare_meter_tooltip).to eq('Time remaining: 31h 46m')
end
end
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 176f4a668ff..c000165ccd9 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Group issues page' do
include FilteredSearchHelpers
+ include DragTo
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group)}
@@ -99,4 +100,62 @@ describe 'Group issues page' do
end
end
end
+
+ context 'manual ordering' do
+ let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+
+ let!(:issue1) { create(:issue, project: project, title: 'Issue #1', relative_position: 1) }
+ let!(:issue2) { create(:issue, project: project, title: 'Issue #2', relative_position: 2) }
+ let!(:issue3) { create(:issue, project: project, title: 'Issue #3', relative_position: 3) }
+
+ before do
+ sign_in(user_in_group)
+ end
+
+ it 'displays all issues' do
+ visit issues_group_path(group, sort: 'relative_position')
+
+ page.within('.issues-list') do
+ expect(page).to have_selector('li.issue', count: 3)
+ end
+ end
+
+ it 'has manual-ordering css applied' do
+ visit issues_group_path(group, sort: 'relative_position')
+
+ expect(page).to have_selector('.manual-ordering')
+ end
+
+ it 'each issue item has a user-can-drag css applied' do
+ visit issues_group_path(group, sort: 'relative_position')
+
+ page.within('.manual-ordering') do
+ expect(page).to have_selector('.issue.user-can-drag', count: 3)
+ end
+ end
+
+ it 'issues should be draggable and persist order', :js do
+ visit issues_group_path(group, sort: 'relative_position')
+
+ drag_to(selector: '.manual-ordering',
+ from_index: 0,
+ to_index: 2)
+
+ wait_for_requests
+
+ check_issue_order
+
+ visit issues_group_path(group, sort: 'relative_position')
+
+ check_issue_order
+ end
+
+ def check_issue_order
+ page.within('.manual-ordering') do
+ expect(find('.issue:nth-child(1) .title')).to have_content('Issue #2')
+ expect(find('.issue:nth-child(2) .title')).to have_content('Issue #3')
+ expect(find('.issue:nth-child(3) .title')).to have_content('Issue #1')
+ end
+ end
+ end
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 8eb413bdd8d..40845ec48f9 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -3,14 +3,14 @@ require 'rails_helper'
describe 'GFM autocomplete', :js do
let(:issue_xss_title) { 'This will execute alert<img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;' }
let(:user_xss_title) { 'eve <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;' }
- let(:label_xss_title) { 'alert label &lt;img src=x onerror="alert(\'Hello xss\');" a'}
+ let(:label_xss_title) { 'alert label &lt;img src=x onerror="alert(\'Hello xss\');" a' }
let(:milestone_xss_title) { 'alert milestone &lt;img src=x onerror="alert(\'Hello xss\');" a' }
let(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') }
- let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
+ let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let(:project) { create(:project) }
let(:label) { create(:label, project: project, title: 'special+') }
- let(:issue) { create(:issue, project: project) }
+ let(:issue) { create(:issue, project: project) }
before do
project.add_maintainer(user)
@@ -293,6 +293,70 @@ describe 'GFM autocomplete', :js do
expect(find('.atwho-view-ul').text).to have_content('alert label')
end
end
+
+ it 'allows colons when autocompleting scoped labels' do
+ create(:label, project: project, title: 'scoped:label')
+
+ note = find('#note-body')
+ type(note, '~scoped:')
+
+ wait_for_requests
+
+ page.within '.atwho-container #at-view-labels' do
+ expect(find('.atwho-view-ul').text).to have_content('scoped:label')
+ end
+ end
+
+ it 'allows colons when autocompleting scoped labels with double colons' do
+ create(:label, project: project, title: 'scoped::label')
+
+ note = find('#note-body')
+ type(note, '~scoped::')
+
+ wait_for_requests
+
+ page.within '.atwho-container #at-view-labels' do
+ expect(find('.atwho-view-ul').text).to have_content('scoped::label')
+ end
+ end
+
+ it 'allows spaces when autocompleting multi-word labels' do
+ create(:label, project: project, title: 'Accepting merge requests')
+
+ note = find('#note-body')
+ type(note, '~Accepting merge')
+
+ wait_for_requests
+
+ page.within '.atwho-container #at-view-labels' do
+ expect(find('.atwho-view-ul').text).to have_content('Accepting merge requests')
+ end
+ end
+
+ it 'only autocompletes the latest label' do
+ create(:label, project: project, title: 'Accepting merge requests')
+ create(:label, project: project, title: 'Accepting job applicants')
+
+ note = find('#note-body')
+ type(note, '~Accepting merge requests foo bar ~Accepting job')
+
+ wait_for_requests
+
+ page.within '.atwho-container #at-view-labels' do
+ expect(find('.atwho-view-ul').text).to have_content('Accepting job applicants')
+ end
+ end
+
+ it 'does not autocomplete labels if no tilde is typed' do
+ create(:label, project: project, title: 'Accepting merge requests')
+
+ note = find('#note-body')
+ type(note, 'Accepting merge')
+
+ wait_for_requests
+
+ expect(page).not_to have_css('.atwho-container #at-view-labels')
+ end
end
shared_examples 'autocomplete suggestions' do
diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb
index edbab14f7c1..b08ccdc2a7c 100644
--- a/spec/features/projects/environments/environment_metrics_spec.rb
+++ b/spec/features/projects/environments/environment_metrics_spec.rb
@@ -9,11 +9,11 @@ describe 'Environment > Metrics' do
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:environment) { create(:environment, project: project) }
let(:current_time) { Time.now.utc }
+ let!(:staging) { create(:environment, name: 'staging', project: project) }
before do
project.add_developer(user)
- create(:deployment, environment: environment, deployable: build)
- stub_all_prometheus_requests(environment.slug)
+ stub_any_prometheus_request
sign_in(user)
visit_environment(environment)
@@ -23,15 +23,50 @@ describe 'Environment > Metrics' do
Timecop.freeze(current_time) { example.run }
end
+ shared_examples 'has environment selector' do
+ it 'has a working environment selector', :js do
+ click_link('See metrics')
+
+ expect(page).to have_metrics_path(environment)
+ expect(page).to have_css('div.js-environments-dropdown')
+
+ within('div.js-environments-dropdown') do
+ # Click on the dropdown
+ click_on(environment.name)
+
+ # Select the staging environment
+ click_on(staging.name)
+ end
+
+ expect(page).to have_metrics_path(staging)
+
+ wait_for_requests
+ end
+ end
+
+ context 'without deployments' do
+ it_behaves_like 'has environment selector'
+ end
+
context 'with deployments and related deployable present' do
+ before do
+ create(:deployment, environment: environment, deployable: build)
+ end
+
it 'shows metrics' do
click_link('See metrics')
expect(page).to have_css('div#prometheus-graphs')
end
+
+ it_behaves_like 'has environment selector'
end
def visit_environment(environment)
visit project_environment_path(environment.project, environment)
end
+
+ def have_metrics_path(environment)
+ have_current_path(metrics_project_environment_path(project, id: environment.id))
+ end
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 683268d064a..e0fa9dbb5fa 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -118,19 +118,31 @@ describe 'Projects > Files > User edits files', :js do
wait_for_requests
end
- it 'inserts a content of a file in a forked project' do
- click_link('.gitignore')
- find('.js-edit-blob').click
-
+ def expect_fork_prompt
expect(page).to have_link('Fork')
expect(page).to have_button('Cancel')
+ expect(page).to have_content(
+ "You're not allowed to edit files in this project directly. "\
+ "Please fork this project, make your changes there, and submit a merge request."
+ )
+ end
- click_link('Fork')
-
+ def expect_fork_status
expect(page).to have_content(
"You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
)
+ end
+
+ it 'inserts a content of a file in a forked project' do
+ click_link('.gitignore')
+ click_button('Edit')
+
+ expect_fork_prompt
+
+ click_link('Fork')
+
+ expect_fork_status
find('.file-editor', match: :first)
@@ -140,12 +152,24 @@ describe 'Projects > Files > User edits files', :js do
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
end
+ it 'opens the Web IDE in a forked project' do
+ click_link('.gitignore')
+ click_button('Web IDE')
+
+ expect_fork_prompt
+
+ click_link('Fork')
+
+ expect_fork_status
+
+ expect(page).to have_css('.ide .multi-file-tab', text: '.gitignore')
+ end
+
it 'commits an edited file in a forked project' do
click_link('.gitignore')
find('.js-edit-blob').click
- expect(page).to have_link('Fork')
- expect(page).to have_button('Cancel')
+ expect_fork_prompt
click_link('Fork')
diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb
index 9a049764dec..a4dd79b3179 100644
--- a/spec/features/raven_js_spec.rb
+++ b/spec/features/raven_js_spec.rb
@@ -10,7 +10,7 @@ describe 'RavenJS' do
end
it 'loads raven if sentry is enabled' do
- stub_application_setting(clientside_sentry_dsn: 'https://key@domain.com/id', clientside_sentry_enabled: true)
+ stub_sentry_settings
visit new_user_session_path
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 8a6901ea4e9..50befa7028d 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -90,7 +90,7 @@ describe 'Signup' do
expect(page).to have_content("Invalid input, please avoid emojis")
end
- it 'shows a pending message if the username availability is being fetched' do
+ it 'shows a pending message if the username availability is being fetched', :quarantine do
fill_in 'new_user_username', with: 'new-user'
expect(find('.username > .validation-pending')).not_to have_css '.hide'
diff --git a/spec/frontend/boards/modal_store_spec.js b/spec/frontend/boards/modal_store_spec.js
index 4dd27e94d97..5b5ae4b6556 100644
--- a/spec/frontend/boards/modal_store_spec.js
+++ b/spec/frontend/boards/modal_store_spec.js
@@ -25,7 +25,7 @@ describe('Modal store', () => {
});
issue2 = new ListIssue({
title: 'Testing',
- id: 1,
+ id: 2,
iid: 2,
confidential: false,
labels: [],
diff --git a/spec/frontend/boards/services/board_service_spec.js b/spec/frontend/boards/services/board_service_spec.js
new file mode 100644
index 00000000000..de9fc998360
--- /dev/null
+++ b/spec/frontend/boards/services/board_service_spec.js
@@ -0,0 +1,390 @@
+import BoardService from '~/boards/services/board_service';
+import { TEST_HOST } from 'helpers/test_constants';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+
+describe('BoardService', () => {
+ const dummyResponse = "without type checking this doesn't matter";
+ const boardId = 'dummy-board-id';
+ const endpoints = {
+ boardsEndpoint: `${TEST_HOST}/boards`,
+ listsEndpoint: `${TEST_HOST}/lists`,
+ bulkUpdatePath: `${TEST_HOST}/bulk/update`,
+ recentBoardsEndpoint: `${TEST_HOST}/recent/boards`,
+ };
+
+ let service;
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ service = new BoardService({
+ ...endpoints,
+ boardId,
+ });
+ });
+
+ describe('all', () => {
+ it('makes a request to fetch lists', () => {
+ axiosMock.onGet(endpoints.listsEndpoint).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.all()).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(endpoints.listsEndpoint).replyOnce(500);
+
+ return expect(service.all()).rejects.toThrow();
+ });
+ });
+
+ describe('generateDefaultLists', () => {
+ const listsEndpointGenerate = `${endpoints.listsEndpoint}/generate.json`;
+
+ it('makes a request to generate default lists', () => {
+ axiosMock.onPost(listsEndpointGenerate).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.generateDefaultLists()).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onPost(listsEndpointGenerate).replyOnce(500);
+
+ return expect(service.generateDefaultLists()).rejects.toThrow();
+ });
+ });
+
+ describe('createList', () => {
+ const entityType = 'moorhen';
+ const entityId = 'quack';
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({ list: { [entityType]: entityId } }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPost(endpoints.listsEndpoint).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to create a list', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.createList(entityId, entityType))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.createList(entityId, entityType))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('updateList', () => {
+ const id = 'David Webb';
+ const position = 'unknown';
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({ list: { position } }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPut(`${endpoints.listsEndpoint}/${id}`).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to update a list position', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.updateList(id, position))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.updateList(id, position))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('destroyList', () => {
+ const id = '-42';
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock
+ .onDelete(`${endpoints.listsEndpoint}/${id}`)
+ .replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to delete a list', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.destroyList(id))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.destroyList(id))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('getIssuesForList', () => {
+ const id = 'TOO-MUCH';
+ const url = `${endpoints.listsEndpoint}/${id}/issues?id=${id}`;
+
+ it('makes a request to fetch list issues', () => {
+ axiosMock.onGet(url).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.getIssuesForList(id)).resolves.toEqual(expectedResponse);
+ });
+
+ it('makes a request to fetch list issues with filter', () => {
+ const filter = { algal: 'scrubber' };
+ axiosMock.onGet(`${url}&algal=scrubber`).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.getIssuesForList(id, filter)).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(url).replyOnce(500);
+
+ return expect(service.getIssuesForList(id)).rejects.toThrow();
+ });
+ });
+
+ describe('moveIssue', () => {
+ const urlRoot = 'potato';
+ const id = 'over 9000';
+ const fromListId = 'left';
+ const toListId = 'right';
+ const moveBeforeId = 'up';
+ const moveAfterId = 'down';
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({
+ from_list_id: fromListId,
+ to_list_id: toListId,
+ move_before_id: moveBeforeId,
+ move_after_id: moveAfterId,
+ }),
+ });
+
+ let requestSpy;
+
+ beforeAll(() => {
+ global.gon.relative_url_root = urlRoot;
+ });
+
+ afterAll(() => {
+ delete global.gon.relative_url_root;
+ });
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock
+ .onPut(`${urlRoot}/-/boards/${boardId}/issues/${id}`)
+ .replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to move an issue between lists', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('newIssue', () => {
+ const id = 'not-creative';
+ const issue = { some: 'issue data' };
+ const url = `${endpoints.listsEndpoint}/${id}/issues`;
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({
+ issue,
+ }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPost(url).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to create a new issue', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.newIssue(id, issue))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.newIssue(id, issue))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('getBacklog', () => {
+ const urlRoot = 'deep';
+ const url = `${urlRoot}/-/boards/${boardId}/issues.json?not=relevant`;
+ const requestParams = {
+ not: 'relevant',
+ };
+
+ beforeAll(() => {
+ global.gon.relative_url_root = urlRoot;
+ });
+
+ afterAll(() => {
+ delete global.gon.relative_url_root;
+ });
+
+ it('makes a request to fetch backlog', () => {
+ axiosMock.onGet(url).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.getBacklog(requestParams)).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(url).replyOnce(500);
+
+ return expect(service.getBacklog(requestParams)).rejects.toThrow();
+ });
+ });
+
+ describe('bulkUpdate', () => {
+ const issueIds = [1, 2, 3];
+ const extraData = { moar: 'data' };
+ const expectedRequest = expect.objectContaining({
+ data: JSON.stringify({
+ update: {
+ ...extraData,
+ issuable_ids: '1,2,3',
+ },
+ }),
+ });
+
+ let requestSpy;
+
+ beforeEach(() => {
+ requestSpy = jest.fn();
+ axiosMock.onPost(endpoints.bulkUpdatePath).replyOnce(config => requestSpy(config));
+ });
+
+ it('makes a request to create a list', () => {
+ requestSpy.mockReturnValue([200, dummyResponse]);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(service.bulkUpdate(issueIds, extraData))
+ .resolves.toEqual(expectedResponse)
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+
+ it('fails for error response', () => {
+ requestSpy.mockReturnValue([500]);
+
+ return expect(service.bulkUpdate(issueIds, extraData))
+ .rejects.toThrow()
+ .then(() => {
+ expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
+ });
+ });
+ });
+
+ describe('getIssueInfo', () => {
+ const dummyEndpoint = `${TEST_HOST}/some/where`;
+
+ it('makes a request to the given endpoint', () => {
+ axiosMock.onGet(dummyEndpoint).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(BoardService.getIssueInfo(dummyEndpoint)).resolves.toEqual(expectedResponse);
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onGet(dummyEndpoint).replyOnce(500);
+
+ return expect(BoardService.getIssueInfo(dummyEndpoint)).rejects.toThrow();
+ });
+ });
+
+ describe('toggleIssueSubscription', () => {
+ const dummyEndpoint = `${TEST_HOST}/some/where`;
+
+ it('makes a request to the given endpoint', () => {
+ axiosMock.onPost(dummyEndpoint).replyOnce(200, dummyResponse);
+ const expectedResponse = expect.objectContaining({ data: dummyResponse });
+
+ return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).resolves.toEqual(
+ expectedResponse,
+ );
+ });
+
+ it('fails for error response', () => {
+ axiosMock.onPost(dummyEndpoint).replyOnce(500);
+
+ return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow();
+ });
+ });
+});
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
index c146ef79be7..8632c5c4e26 100644
--- a/spec/frontend/clusters/services/application_state_machine_spec.js
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -72,9 +72,10 @@ describe('applicationStateMachine', () => {
describe(`current state is ${INSTALLABLE}`, () => {
it.each`
- expectedState | event | effects
- ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
- ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ expectedState | event | effects
+ ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
@@ -108,9 +109,10 @@ describe('applicationStateMachine', () => {
describe(`current state is ${INSTALLED}`, () => {
it.each`
- expectedState | event | effects
- ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
- ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
+ expectedState | event | effects
+ ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
+ ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
@@ -119,7 +121,7 @@ describe('applicationStateMachine', () => {
expect(transitionApplicationState(currentAppState, event)).toEqual({
status: expectedState,
- ...effects,
+ ...noEffectsToEmptyObject(effects),
});
});
});
diff --git a/spec/frontend/helpers/vuex_action_helper.js b/spec/frontend/helpers/vuex_action_helper.js
index 88652202a8e..6c3569a2247 100644
--- a/spec/frontend/helpers/vuex_action_helper.js
+++ b/spec/frontend/helpers/vuex_action_helper.js
@@ -20,7 +20,7 @@ const noop = () => {};
* // expected mutations
* [
* { type: types.MUTATION}
- * { type: types.MUTATION_1, payload: jasmine.any(Number)}
+ * { type: types.MUTATION_1, payload: expect.any(Number)}
* ],
* // expected actions
* [
@@ -89,10 +89,7 @@ export default (
payload,
);
- return new Promise(resolve => {
- setImmediate(resolve);
- })
- .then(() => result)
+ return (result || new Promise(resolve => setImmediate(resolve)))
.catch(error => {
validateResults();
throw error;
diff --git a/spec/frontend/helpers/vuex_action_helper_spec.js b/spec/frontend/helpers/vuex_action_helper_spec.js
new file mode 100644
index 00000000000..61d05762a04
--- /dev/null
+++ b/spec/frontend/helpers/vuex_action_helper_spec.js
@@ -0,0 +1,166 @@
+import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import testAction from './vuex_action_helper';
+
+describe('VueX test helper (testAction)', () => {
+ let originalExpect;
+ let assertion;
+ let mock;
+ const noop = () => {};
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ /**
+ * In order to test the helper properly, we need to overwrite the Jest
+ * `expect` helper. We test that the testAction helper properly passes the
+ * dispatched actions/committed mutations to the Jest helper.
+ */
+ originalExpect = expect;
+ assertion = null;
+ global.expect = actual => ({
+ toEqual: () => {
+ originalExpect(actual).toEqual(assertion);
+ },
+ });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ global.expect = originalExpect;
+ });
+
+ it('properly passes state and payload to action', () => {
+ const exampleState = { FOO: 12, BAR: 3 };
+ const examplePayload = { BAZ: 73, BIZ: 55 };
+
+ const action = ({ state }, payload) => {
+ originalExpect(state).toEqual(exampleState);
+ originalExpect(payload).toEqual(examplePayload);
+ };
+
+ assertion = { mutations: [], actions: [] };
+
+ testAction(action, examplePayload, exampleState);
+ });
+
+ describe('given a sync action', () => {
+ it('mocks committing mutations', () => {
+ const action = ({ commit }) => {
+ commit('MUTATION');
+ };
+
+ assertion = { mutations: [{ type: 'MUTATION' }], actions: [] };
+
+ testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ });
+
+ it('mocks dispatching actions', () => {
+ const action = ({ dispatch }) => {
+ dispatch('ACTION');
+ };
+
+ assertion = { actions: [{ type: 'ACTION' }], mutations: [] };
+
+ testAction(action, null, {}, assertion.mutations, assertion.actions, noop);
+ });
+
+ it('works with done callback once finished', done => {
+ assertion = { mutations: [], actions: [] };
+
+ testAction(noop, null, {}, assertion.mutations, assertion.actions, done);
+ });
+
+ it('returns a promise', done => {
+ assertion = { mutations: [], actions: [] };
+
+ testAction(noop, null, {}, assertion.mutations, assertion.actions)
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('given an async action (returning a promise)', () => {
+ let lastError;
+ const data = { FOO: 'BAR' };
+
+ const asyncAction = ({ commit, dispatch }) => {
+ dispatch('ACTION');
+
+ return axios
+ .get(TEST_HOST)
+ .catch(error => {
+ commit('ERROR');
+ lastError = error;
+ throw error;
+ })
+ .then(() => {
+ commit('SUCCESS');
+ return data;
+ });
+ };
+
+ beforeEach(() => {
+ lastError = null;
+ });
+
+ it('works with done callback once finished', done => {
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
+
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done);
+ });
+
+ it('returns original data of successful promise while checking actions/mutations', done => {
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
+
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions)
+ .then(res => {
+ originalExpect(res).toEqual(data);
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('returns original error of rejected promise while checking actions/mutations', done => {
+ mock.onGet(TEST_HOST).replyOnce(500, '');
+
+ assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions)
+ .then(done.fail)
+ .catch(error => {
+ originalExpect(error).toBe(lastError);
+ done();
+ });
+ });
+ });
+
+ it('works with async actions not returning promises', done => {
+ const data = { FOO: 'BAR' };
+
+ const asyncAction = ({ commit, dispatch }) => {
+ dispatch('ACTION');
+
+ axios
+ .get(TEST_HOST)
+ .then(() => {
+ commit('SUCCESS');
+ return data;
+ })
+ .catch(error => {
+ commit('ERROR');
+ throw error;
+ });
+ };
+
+ mock.onGet(TEST_HOST).replyOnce(200, 42);
+
+ assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
+
+ testAction(asyncAction, null, {}, assertion.mutations, assertion.actions, done);
+ });
+});
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
new file mode 100644
index 00000000000..2b7dffdcd88
--- /dev/null
+++ b/spec/frontend/ide/utils_spec.js
@@ -0,0 +1,44 @@
+import { commitItemIconMap } from '~/ide/constants';
+import { getCommitIconMap } from '~/ide/utils';
+import { decorateData } from '~/ide/stores/utils';
+
+describe('WebIDE utils', () => {
+ const createFile = (name = 'name', id = name, type = '', parent = null) =>
+ decorateData({
+ id,
+ type,
+ icon: 'icon',
+ url: 'url',
+ name,
+ path: parent ? `${parent.path}/${name}` : name,
+ parentPath: parent ? parent.path : '',
+ lastCommit: {},
+ });
+
+ describe('getCommitIconMap', () => {
+ let entry;
+
+ beforeEach(() => {
+ entry = createFile('Entry item');
+ });
+
+ it('renders "deleted" icon for deleted entries', () => {
+ entry.deleted = true;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.deleted);
+ });
+ it('renders "addition" icon for temp entries', () => {
+ entry.tempFile = true;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.addition);
+ });
+ it('renders "modified" icon for newly-renamed entries', () => {
+ entry.prevPath = 'foo/bar';
+ entry.tempFile = false;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified);
+ });
+ it('renders "modified" icon even for temp entries if they are newly-renamed', () => {
+ entry.prevPath = 'foo/bar';
+ entry.tempFile = true;
+ expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 9f49e68cfe8..751fb5e1b94 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -334,6 +334,12 @@ describe('prettyTime methods', () => {
assertTimeUnits(aboveOneDay, 33, 2, 2, 0);
assertTimeUnits(aboveOneWeek, 26, 0, 1, 9);
});
+
+ it('should correctly parse values when limitedToHours is true', () => {
+ const twoDays = datetimeUtility.parseSeconds(173000, { limitToHours: true });
+
+ assertTimeUnits(twoDays, 3, 48, 0, 0);
+ });
});
describe('stringifyTime', () => {
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 1f06d693411..d55dc553031 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -29,10 +29,20 @@ exports[`Repository table row component renders table row 1`] = `
<td
class="d-none d-sm-table-cell tree-commit"
- />
+ >
+ <glskeletonloading-stub
+ class="h-auto"
+ lines="1"
+ />
+ </td>
<td
class="tree-time-ago text-right"
- />
+ >
+ <glskeletonloading-stub
+ class="ml-auto h-auto w-50"
+ lines="1"
+ />
+ </td>
</tr>
`;
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 5a345ddeacd..c566057ad3f 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -16,6 +16,8 @@ function factory(propsData = {}) {
vm = shallowMount(TableRow, {
propsData: {
...propsData,
+ name: propsData.path,
+ projectPath: 'gitlab-org/gitlab-ce',
url: `https://test.com`,
},
mocks: {
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
new file mode 100644
index 00000000000..a9499f7c61b
--- /dev/null
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -0,0 +1,129 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { normalizeData, resolveCommit, fetchLogsTree } from '~/repository/log_tree';
+
+const mockData = [
+ {
+ commit: {
+ id: '123',
+ message: 'testing message',
+ committed_date: '2019-01-01',
+ },
+ commit_path: `https://test.com`,
+ file_name: 'index.js',
+ type: 'blob',
+ },
+];
+
+describe('normalizeData', () => {
+ it('normalizes data into LogTreeCommit object', () => {
+ expect(normalizeData(mockData)).toEqual([
+ {
+ sha: '123',
+ message: 'testing message',
+ committedDate: '2019-01-01',
+ commitPath: 'https://test.com',
+ fileName: 'index.js',
+ type: 'blob',
+ __typename: 'LogTreeCommit',
+ },
+ ]);
+ });
+});
+
+describe('resolveCommit', () => {
+ it('calls resolve when commit found', () => {
+ const resolver = {
+ entry: { name: 'index.js', type: 'blob' },
+ resolve: jest.fn(),
+ };
+ const commits = [{ fileName: 'index.js', type: 'blob' }];
+
+ resolveCommit(commits, resolver);
+
+ expect(resolver.resolve).toHaveBeenCalledWith({ fileName: 'index.js', type: 'blob' });
+ });
+});
+
+describe('fetchLogsTree', () => {
+ let mock;
+ let client;
+ let resolver;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onGet(/(.*)/).reply(200, mockData, {});
+
+ jest.spyOn(axios, 'get');
+
+ global.gon = { gitlab_url: 'https://test.com' };
+
+ client = {
+ readQuery: () => ({
+ projectPath: 'gitlab-org/gitlab-ce',
+ ref: 'master',
+ commits: [],
+ }),
+ writeQuery: jest.fn(),
+ };
+
+ resolver = {
+ entry: { name: 'index.js', type: 'blob' },
+ resolve: jest.fn(),
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('calls axios get', () =>
+ fetchLogsTree(client, '', '0', resolver).then(() => {
+ expect(axios.get).toHaveBeenCalledWith(
+ 'https://test.com/gitlab-org/gitlab-ce/refs/master/logs_tree',
+ { params: { format: 'json', offset: '0' } },
+ );
+ }));
+
+ it('calls axios get once', () =>
+ Promise.all([
+ fetchLogsTree(client, '', '0', resolver),
+ fetchLogsTree(client, '', '0', resolver),
+ ]).then(() => {
+ expect(axios.get.mock.calls.length).toEqual(1);
+ }));
+
+ it('calls entry resolver', () =>
+ fetchLogsTree(client, '', '0', resolver).then(() => {
+ expect(resolver.resolve).toHaveBeenCalledWith({
+ __typename: 'LogTreeCommit',
+ commitPath: 'https://test.com',
+ committedDate: '2019-01-01',
+ fileName: 'index.js',
+ message: 'testing message',
+ sha: '123',
+ type: 'blob',
+ });
+ }));
+
+ it('writes query to client', () =>
+ fetchLogsTree(client, '', '0', resolver).then(() => {
+ expect(client.writeQuery).toHaveBeenCalledWith({
+ query: expect.anything(),
+ data: {
+ commits: [
+ {
+ __typename: 'LogTreeCommit',
+ commitPath: 'https://test.com',
+ committedDate: '2019-01-01',
+ fileName: 'index.js',
+ message: 'testing message',
+ sha: '123',
+ type: 'blob',
+ },
+ ],
+ },
+ });
+ }));
+});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 7e7cc1488b8..c17d5253997 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -1,10 +1,16 @@
import Vue from 'vue';
import * as jqueryMatchers from 'custom-jquery-matchers';
+import $ from 'jquery';
import Translate from '~/vue_shared/translate';
import axios from '~/lib/utils/axios_utils';
import { initializeTestTimeout } from './helpers/timeout';
import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures';
+// Expose jQuery so specs using jQuery plugins can be imported nicely.
+// Here is an issue to explore better alternatives:
+// https://gitlab.com/gitlab-org/gitlab-ee/issues/12448
+window.jQuery = $;
+
process.on('unhandledRejection', global.promiseRejectionHandler);
afterEach(() =>
diff --git a/spec/helpers/recaptcha_experiment_helper_spec.rb b/spec/helpers/recaptcha_experiment_helper_spec.rb
new file mode 100644
index 00000000000..775c2caa082
--- /dev/null
+++ b/spec/helpers/recaptcha_experiment_helper_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe RecaptchaExperimentHelper, type: :helper do
+ describe '.show_recaptcha_sign_up?' do
+ context 'when reCAPTCHA is disabled' do
+ it 'returns false' do
+ stub_application_setting(recaptcha_enabled: false)
+
+ expect(helper.show_recaptcha_sign_up?).to be(false)
+ end
+ end
+
+ context 'when reCAPTCHA is enabled' do
+ it 'returns true' do
+ stub_application_setting(recaptcha_enabled: true)
+
+ expect(helper.show_recaptcha_sign_up?).to be(true)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index da14f7f16fb..c69493b579f 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -114,7 +114,7 @@ describe SearchHelper do
end
it 'includes project endpoints' do
- expect(search_filter_input_options('')[:data]['base-endpoint']).to eq(project_path(@project))
+ expect(search_filter_input_options('')[:data]['runner-tags-endpoint']).to eq(tag_list_admin_runners_path)
expect(search_filter_input_options('')[:data]['labels-endpoint']).to eq(project_labels_path(@project))
expect(search_filter_input_options('')[:data]['milestones-endpoint']).to eq(project_milestones_path(@project))
end
@@ -134,7 +134,7 @@ describe SearchHelper do
end
it 'includes group endpoints' do
- expect(search_filter_input_options('')[:data]['base-endpoint']).to eq("/groups#{group_path(@group)}")
+ expect(search_filter_input_options('')[:data]['runner-tags-endpoint']).to eq(tag_list_admin_runners_path)
expect(search_filter_input_options('')[:data]['labels-endpoint']).to eq(group_labels_path(@group))
expect(search_filter_input_options('')[:data]['milestones-endpoint']).to eq(group_milestones_path(@group))
end
@@ -147,7 +147,7 @@ describe SearchHelper do
end
it 'includes dashboard endpoints' do
- expect(search_filter_input_options('')[:data]['base-endpoint']).to eq("/dashboard")
+ expect(search_filter_input_options('')[:data]['runner-tags-endpoint']).to eq(tag_list_admin_runners_path)
expect(search_filter_input_options('')[:data]['labels-endpoint']).to eq(dashboard_labels_path)
expect(search_filter_input_options('')[:data]['milestones-endpoint']).to eq(dashboard_milestones_path)
end
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index e81115e10c9..5266b1bdbfc 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -355,4 +355,14 @@ describe('Store', () => {
expect(boardsStore.moving.list).toEqual(dummyList);
});
});
+
+ describe('setTimeTrackingLimitToHours', () => {
+ it('sets the timeTracking.LimitToHours option', () => {
+ boardsStore.timeTracking.limitToHours = false;
+
+ boardsStore.setTimeTrackingLimitToHours('true');
+
+ expect(boardsStore.timeTracking.limitToHours).toEqual(true);
+ });
+ });
});
diff --git a/spec/javascripts/boards/components/issue_time_estimate_spec.js b/spec/javascripts/boards/components/issue_time_estimate_spec.js
index ba65d3287da..de48e3f6091 100644
--- a/spec/javascripts/boards/components/issue_time_estimate_spec.js
+++ b/spec/javascripts/boards/components/issue_time_estimate_spec.js
@@ -1,40 +1,70 @@
import Vue from 'vue';
import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
+import boardsStore from '~/boards/stores/boards_store';
import mountComponent from '../../helpers/vue_mount_component_helper';
-describe('Issue Tine Estimate component', () => {
+describe('Issue Time Estimate component', () => {
let vm;
beforeEach(() => {
- const Component = Vue.extend(IssueTimeEstimate);
- vm = mountComponent(Component, {
- estimate: 374460,
- });
+ boardsStore.create();
});
afterEach(() => {
vm.$destroy();
});
- it('renders the correct time estimate', () => {
- expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m');
- });
+ describe('when limitToHours is false', () => {
+ beforeEach(() => {
+ boardsStore.timeTracking.limitToHours = false;
+
+ const Component = Vue.extend(IssueTimeEstimate);
+ vm = mountComponent(Component, {
+ estimate: 374460,
+ });
+ });
+
+ it('renders the correct time estimate', () => {
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m');
+ });
+
+ it('renders expanded time estimate in tooltip', () => {
+ expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
+ '2 weeks 3 days 1 minute',
+ );
+ });
+
+ it('prevents tooltip xss', done => {
+ const alertSpy = spyOn(window, 'alert');
+ vm.estimate = 'Foo <script>alert("XSS")</script>';
- it('renders expanded time estimate in tooltip', () => {
- expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
- '2 weeks 3 days 1 minute',
- );
+ vm.$nextTick(() => {
+ expect(alertSpy).not.toHaveBeenCalled();
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
+ expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
+ done();
+ });
+ });
});
- it('prevents tooltip xss', done => {
- const alertSpy = spyOn(window, 'alert');
- vm.estimate = 'Foo <script>alert("XSS")</script>';
+ describe('when limitToHours is true', () => {
+ beforeEach(() => {
+ boardsStore.timeTracking.limitToHours = true;
+
+ const Component = Vue.extend(IssueTimeEstimate);
+ vm = mountComponent(Component, {
+ estimate: 374460,
+ });
+ });
+
+ it('renders the correct time estimate', () => {
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('104h 1m');
+ });
- vm.$nextTick(() => {
- expect(alertSpy).not.toHaveBeenCalled();
- expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
- expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
- done();
+ it('renders expanded time estimate in tooltip', () => {
+ expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
+ '104 hours 1 minute',
+ );
});
});
});
diff --git a/spec/javascripts/filtered_search/visual_token_value_spec.js b/spec/javascripts/filtered_search/visual_token_value_spec.js
index d1d16afc977..10d844fd94b 100644
--- a/spec/javascripts/filtered_search/visual_token_value_spec.js
+++ b/spec/javascripts/filtered_search/visual_token_value_spec.js
@@ -155,7 +155,7 @@ describe('Filtered Search Visual Tokens', () => {
`);
const filteredSearchInput = document.querySelector('.filtered-search');
- filteredSearchInput.dataset.baseEndpoint = dummyEndpoint;
+ filteredSearchInput.dataset.runnerTagsEndpoint = `${dummyEndpoint}/admin/runners/tag_list`;
filteredSearchInput.dataset.labelsEndpoint = `${dummyEndpoint}/-/labels`;
filteredSearchInput.dataset.milestonesEndpoint = `${dummyEndpoint}/-/milestones`;
diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js
index f63007c7dd2..554bd1ae3b5 100644
--- a/spec/javascripts/ide/components/ide_tree_list_spec.js
+++ b/spec/javascripts/ide/components/ide_tree_list_spec.js
@@ -58,6 +58,20 @@ describe('IDE tree list', () => {
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
});
+
+ it('does not render moved entries', done => {
+ const tree = [file('moved entry'), file('normal entry')];
+ tree[0].moved = true;
+ store.state.trees['abcproject/master'].tree = tree;
+ const container = vm.$el.querySelector('.ide-tree-body');
+
+ vm.$nextTick(() => {
+ expect(container.children.length).toBe(1);
+ expect(vm.$el.textContent).not.toContain('moved entry');
+ expect(vm.$el.textContent).toContain('normal entry');
+ done();
+ });
+ });
});
describe('empty-branch state', () => {
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index dd2313dc800..021c3076094 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -275,6 +275,43 @@ describe('IDE store file actions', () => {
});
});
+ describe('Re-named success', () => {
+ beforeEach(() => {
+ localFile = file(`newCreate-${Math.random()}`);
+ localFile.url = `project/getFileDataURL`;
+ localFile.prevPath = 'old-dull-file';
+ localFile.path = 'new-shiny-file';
+ store.state.entries[localFile.path] = localFile;
+
+ mock.onGet(`${RELATIVE_URL_ROOT}/project/getFileDataURL`).replyOnce(
+ 200,
+ {
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ },
+ {
+ 'page-title': 'testing old-dull-file',
+ },
+ );
+ });
+
+ it('sets document title considering `prevPath` on a file', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(document.title).toBe('testing new-shiny-file');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
describe('error', () => {
beforeEach(() => {
mock.onGet(`project/getFileDataURL`).networkError();
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 37354283cab..2d105103c1c 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -492,6 +492,33 @@ describe('Multi-file store actions', () => {
done,
);
});
+
+ it('does not delete a folder after it is emptied', done => {
+ const testFolder = {
+ type: 'tree',
+ tree: [],
+ };
+ const testEntry = {
+ path: 'testFolder/entry-to-delete',
+ parentPath: 'testFolder',
+ opened: false,
+ tree: [],
+ };
+ testFolder.tree.push(testEntry);
+ store.state.entries = {
+ testFolder,
+ 'testFolder/entry-to-delete': testEntry,
+ };
+
+ testAction(
+ deleteEntry,
+ 'testFolder/entry-to-delete',
+ store.state,
+ [{ type: types.DELETE_ENTRY, payload: 'testFolder/entry-to-delete' }],
+ [{ type: 'burstUnusedSeal' }, { type: 'triggerFilesChange' }],
+ done,
+ );
+ });
});
describe('renameEntry', () => {
@@ -509,8 +536,15 @@ describe('Multi-file store actions', () => {
type: types.RENAME_ENTRY,
payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
},
+ {
+ type: types.TOGGLE_FILE_CHANGED,
+ payload: {
+ file: store.state.entries['parent-path/new-name'],
+ changed: true,
+ },
+ },
],
- [{ type: 'deleteEntry', payload: 'test' }, { type: 'triggerFilesChange' }],
+ [{ type: 'triggerFilesChange' }],
done,
);
});
@@ -557,7 +591,6 @@ describe('Multi-file store actions', () => {
parentPath: 'parent-path/new-name',
},
},
- { type: 'deleteEntry', payload: 'test' },
{ type: 'triggerFilesChange' },
],
done,
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 5ee098bf17f..460c5b01081 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -309,7 +309,7 @@ describe('Multi-file store mutations', () => {
...localState.entries.oldPath,
id: 'newPath',
name: 'newPath',
- key: 'newPath-blob-name',
+ key: 'newPath-blob-oldPath',
path: 'newPath',
tempFile: true,
prevPath: 'oldPath',
@@ -318,6 +318,7 @@ describe('Multi-file store mutations', () => {
url: `${gl.TEST_HOST}/newPath`,
moved: jasmine.anything(),
movedPath: jasmine.anything(),
+ opened: false,
});
});
@@ -349,13 +350,5 @@ describe('Multi-file store mutations', () => {
expect(localState.entries.parentPath.tree.length).toBe(1);
});
-
- it('adds to openFiles if previously opened', () => {
- localState.entries.oldPath.opened = true;
-
- mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
-
- expect(localState.openFiles).toEqual([localState.entries.newPath]);
- });
});
});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index f4166987aed..ab8360193be 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -10,6 +10,7 @@ import {
mockApiEndpoint,
environmentData,
singleGroupResponse,
+ dashboardGitResponse,
} from './mock_data';
const propsData = {
@@ -62,16 +63,34 @@ describe('Dashboard', () => {
});
describe('no metrics are available yet', () => {
- it('shows a getting started empty state when no metrics are present', () => {
+ beforeEach(() => {
component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData },
store,
});
+ });
+ it('shows a getting started empty state when no metrics are present', () => {
expect(component.$el.querySelector('.prometheus-graphs')).toBe(null);
expect(component.emptyState).toEqual('gettingStarted');
});
+
+ it('shows the environment selector', () => {
+ expect(component.$el.querySelector('.js-environments-dropdown')).toBeTruthy();
+ });
+ });
+
+ describe('no data found', () => {
+ it('shows the environment selector dropdown', () => {
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: { ...propsData, showEmptyState: true },
+ store,
+ });
+
+ expect(component.$el.querySelector('.js-environments-dropdown')).toBeTruthy();
+ });
});
describe('requests information to the server', () => {
@@ -150,14 +169,24 @@ describe('Dashboard', () => {
singleGroupResponse,
);
- setTimeout(() => {
- const dropdownMenuEnvironments = component.$el.querySelectorAll(
- '.js-environments-dropdown .dropdown-item',
- );
+ Vue.nextTick()
+ .then(() => {
+ const dropdownMenuEnvironments = component.$el.querySelectorAll(
+ '.js-environments-dropdown .dropdown-item',
+ );
- expect(dropdownMenuEnvironments.length).toEqual(component.environments.length);
- done();
- });
+ expect(component.environments.length).toEqual(environmentData.length);
+ expect(dropdownMenuEnvironments.length).toEqual(component.environments.length);
+
+ Array.from(dropdownMenuEnvironments).forEach((value, index) => {
+ if (environmentData[index].metrics_path) {
+ expect(value).toHaveAttr('href', environmentData[index].metrics_path);
+ }
+ });
+
+ done();
+ })
+ .catch(done.fail);
});
it('hides the environments dropdown list when there is no environments', done => {
@@ -212,7 +241,7 @@ describe('Dashboard', () => {
Vue.nextTick()
.then(() => {
const dropdownItems = component.$el.querySelectorAll(
- '.js-environments-dropdown .dropdown-item[active="true"]',
+ '.js-environments-dropdown .dropdown-item.is-active',
);
expect(dropdownItems.length).toEqual(1);
@@ -281,10 +310,6 @@ describe('Dashboard', () => {
const getTimeDiffSpy = spyOnDependency(Dashboard, 'getTimeDiff');
component.$store.commit(
- `monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`,
- '/environments',
- );
- component.$store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData,
);
@@ -402,4 +427,49 @@ describe('Dashboard', () => {
});
});
});
+
+ describe('Dashboard dropdown', () => {
+ beforeEach(() => {
+ mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
+
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ },
+ store,
+ });
+
+ component.$store.dispatch('monitoringDashboard/setFeatureFlags', {
+ prometheusEndpoint: false,
+ multipleDashboardsEnabled: true,
+ });
+
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
+ environmentData,
+ );
+
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
+ singleGroupResponse,
+ );
+
+ component.$store.commit(
+ `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
+ dashboardGitResponse,
+ );
+ });
+
+ it('shows the dashboard dropdown', done => {
+ setTimeout(() => {
+ const dashboardDropdown = component.$el.querySelector('.js-dashboards-dropdown');
+
+ expect(dashboardDropdown).not.toEqual(null);
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 82e42fe9ade..7bbb215475a 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -922,3 +922,16 @@ export const metricsDashboardResponse = {
},
status: 'success',
};
+
+export const dashboardGitResponse = [
+ {
+ path: 'config/prometheus/common_metrics.yml',
+ display_name: 'Common Metrics',
+ default: true,
+ },
+ {
+ path: '.gitlab/dashboards/super.yml',
+ display_name: 'Custom Dashboard 1',
+ default: false,
+ },
+];
diff --git a/spec/javascripts/monitoring/store/actions_spec.js b/spec/javascripts/monitoring/store/actions_spec.js
index 083a01c4d74..677455275de 100644
--- a/spec/javascripts/monitoring/store/actions_spec.js
+++ b/spec/javascripts/monitoring/store/actions_spec.js
@@ -22,6 +22,7 @@ import {
environmentData,
metricsDashboardResponse,
metricsGroupsAPIResponse,
+ dashboardGitResponse,
} from '../mock_data';
describe('Monitoring store actions', () => {
@@ -212,17 +213,19 @@ describe('Monitoring store actions', () => {
describe('receiveMetricsDashboardSuccess', () => {
let commit;
let dispatch;
+ let state;
beforeEach(() => {
commit = jasmine.createSpy();
dispatch = jasmine.createSpy();
+ state = storeState();
});
it('stores groups ', () => {
const params = {};
const response = metricsDashboardResponse;
- receiveMetricsDashboardSuccess({ commit, dispatch }, { response, params });
+ receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response, params });
expect(commit).toHaveBeenCalledWith(
types.RECEIVE_METRICS_DATA_SUCCESS,
@@ -231,6 +234,18 @@ describe('Monitoring store actions', () => {
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetrics', params);
});
+
+ it('sets the dashboards loaded from the repository', () => {
+ const params = {};
+ const response = metricsDashboardResponse;
+
+ response.all_dashboards = dashboardGitResponse;
+ state.multipleDashboardsEnabled = true;
+
+ receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response, params });
+
+ expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse);
+ });
});
describe('receiveMetricsDashboardFailure', () => {
diff --git a/spec/javascripts/monitoring/store/mutations_spec.js b/spec/javascripts/monitoring/store/mutations_spec.js
index 02ff5847b34..91580366531 100644
--- a/spec/javascripts/monitoring/store/mutations_spec.js
+++ b/spec/javascripts/monitoring/store/mutations_spec.js
@@ -1,7 +1,12 @@
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import state from '~/monitoring/stores/state';
-import { metricsGroupsAPIResponse, deploymentData, metricsDashboardResponse } from '../mock_data';
+import {
+ metricsGroupsAPIResponse,
+ deploymentData,
+ metricsDashboardResponse,
+ dashboardGitResponse,
+} from '../mock_data';
describe('Monitoring mutations', () => {
let stateCopy;
@@ -156,4 +161,12 @@ describe('Monitoring mutations', () => {
expect(stateCopy.metricsWithData).toEqual([]);
});
});
+
+ describe('SET_ALL_DASHBOARDS', () => {
+ it('stores the dashboards loaded from the git repository', () => {
+ mutations[types.SET_ALL_DASHBOARDS](stateCopy, dashboardGitResponse);
+
+ expect(stateCopy.allDashboards).toEqual(dashboardGitResponse);
+ });
+ });
});
diff --git a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
index 4c3dd713589..2e1863cff86 100644
--- a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
@@ -13,6 +13,7 @@ describe('Issuable Time Tracker', () => {
timeSpent,
timeEstimateHumanReadable,
timeSpentHumanReadable,
+ limitToHours,
}) => {
setFixtures(`
<div>
@@ -25,6 +26,7 @@ describe('Issuable Time Tracker', () => {
timeSpent,
humanTimeEstimate: timeEstimateHumanReadable,
humanTimeSpent: timeSpentHumanReadable,
+ limitToHours: Boolean(limitToHours),
rootPath: '/',
};
@@ -128,6 +130,29 @@ describe('Issuable Time Tracker', () => {
});
});
+ describe('Comparison pane when limitToHours is true', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 100000, // 1d 3h
+ timeSpent: 5000, // 1h 23m
+ timeEstimateHumanReadable: '',
+ timeSpentHumanReadable: '',
+ limitToHours: true,
+ });
+ });
+
+ it('should show the correct tooltip text', done => {
+ Vue.nextTick(() => {
+ expect(vm.showComparisonState).toBe(true);
+ const $title = vm.$el.querySelector('.time-tracking-content .compare-meter').dataset
+ .originalTitle;
+
+ expect($title).toBe('Time remaining: 26h 23m');
+ done();
+ });
+ });
+ });
+
describe('Estimate only pane', () => {
beforeEach(() => {
initTimeTrackingComponent({
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index e72fb9c6fbc..cceeae8afe6 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -2038,24 +2038,24 @@ describe Gitlab::Git::Repository, :seed_helper do
end
describe '#clean_stale_repository_files' do
- let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') }
+ let(:worktree_id) { 'rebase-1' }
+ let(:gitlab_worktree_path) { File.join(repository_path, 'gitlab-worktree', worktree_id) }
+ let(:admin_dir) { File.join(repository_path, 'worktrees') }
it 'cleans up the files' do
- create_worktree = %W[git -C #{repository_path} worktree add --detach #{worktree_path} master]
+ create_worktree = %W[git -C #{repository_path} worktree add --detach #{gitlab_worktree_path} master]
raise 'preparation failed' unless system(*create_worktree, err: '/dev/null')
- FileUtils.touch(worktree_path, mtime: Time.now - 8.hours)
+ FileUtils.touch(gitlab_worktree_path, mtime: Time.now - 8.hours)
# git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object,
# but the HEAD must be 40 characters long or git will ignore it.
- File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA)
-
- # git 2.16 fails with "fatal: bad object HEAD"
- expect(rev_list_all).to be false
+ File.write(File.join(admin_dir, worktree_id, 'HEAD'), Gitlab::Git::BLANK_SHA)
+ expect(rev_list_all).to be(false)
repository.clean_stale_repository_files
- expect(rev_list_all).to be true
- expect(File.exist?(worktree_path)).to be_falsey
+ expect(rev_list_all).to be(true)
+ expect(File.exist?(gitlab_worktree_path)).to be_falsey
end
def rev_list_all
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 6d6107ca3e7..ba6abba4e61 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -223,6 +223,19 @@ describe Gitlab::GitalyClient::CommitService do
end
context 'when caching of the ref name is enabled' do
+ it 'caches negative entries' do
+ expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).once.and_return(double(commit: nil))
+
+ commit = nil
+ 2.times do
+ ::Gitlab::GitalyClient.allow_ref_name_caching do
+ commit = described_class.new(repository).find_commit('master')
+ end
+ end
+
+ expect(commit).to eq(nil)
+ end
+
it 'returns a cached commit' do
expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).once.and_return(double(commit: commit_dbl))
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
index 13cf52fd795..20842f55014 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
@@ -34,12 +34,6 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do
end
end
- describe '#authorized_find' do
- it 'returns the object' do
- expect(loading_resource.authorized_find).to eq(project)
- end
- end
-
describe '#authorized_find!' do
it 'returns the object' do
expect(loading_resource.authorized_find!).to eq(project)
@@ -66,12 +60,6 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do
end
end
- describe '#authorized_find' do
- it 'returns `nil`' do
- expect(loading_resource.authorized_find).to be_nil
- end
- end
-
describe '#authorized_find!' do
it 'raises an error' do
expect { loading_resource.authorize!(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
@@ -101,6 +89,45 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do
end
end
+ context 'when the class does not define authorize' do
+ let(:fake_class) do
+ Class.new do
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ attr_reader :user, :found_object
+
+ def initialize(user, found_object)
+ @user, @found_object = user, found_object
+ end
+
+ def find_object(*_args)
+ found_object
+ end
+
+ def current_user
+ user
+ end
+
+ def self.name
+ 'TestClass'
+ end
+ end
+ end
+ let(:error) { /#{fake_class.name} has no authorizations/ }
+
+ describe '#authorized_find!' do
+ it 'raises a comprehensive error message' do
+ expect { loading_resource.authorized_find! }.to raise_error(error)
+ end
+ end
+
+ describe '#authorized?' do
+ it 'raises a comprehensive error message' do
+ expect { loading_resource.authorized?(project) }.to raise_error(error)
+ end
+ end
+ end
+
describe '#authorize' do
it 'adds permissions from subclasses to those of superclasses when used on classes' do
base_class = Class.new do
diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb
index 59160741c45..39cdd42088e 100644
--- a/spec/lib/gitlab/json_cache_spec.rb
+++ b/spec/lib/gitlab/json_cache_spec.rb
@@ -129,19 +129,52 @@ describe Gitlab::JsonCache do
.with(expanded_key)
.and_return(nil)
+ expect(ActiveSupport::JSON).not_to receive(:decode)
expect(cache.read(key)).to be_nil
end
- context 'when the cached value is a boolean' do
+ context 'when the cached value is true' do
it 'parses the cached value' do
allow(backend).to receive(:read)
.with(expanded_key)
.and_return(true)
+ expect(ActiveSupport::JSON).to receive(:decode).with("true").and_call_original
expect(cache.read(key, BroadcastMessage)).to eq(true)
end
end
+ context 'when the cached value is false' do
+ it 'parses the cached value' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return(false)
+
+ expect(ActiveSupport::JSON).to receive(:decode).with("false").and_call_original
+ expect(cache.read(key, BroadcastMessage)).to eq(false)
+ end
+ end
+
+ context 'when the cached value is a JSON true value' do
+ it 'parses the cached value' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return("true")
+
+ expect(cache.read(key, BroadcastMessage)).to eq(true)
+ end
+ end
+
+ context 'when the cached value is a JSON false value' do
+ it 'parses the cached value' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return("false")
+
+ expect(cache.read(key, BroadcastMessage)).to eq(false)
+ end
+ end
+
context 'when the cached value is a hash' do
it 'parses the cached value' do
allow(backend).to receive(:read)
diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb
index 5454d9c1af4..cbb862cb0c9 100644
--- a/spec/lib/gitlab/lets_encrypt/client_spec.rb
+++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb
@@ -116,42 +116,6 @@ describe ::Gitlab::LetsEncrypt::Client do
end
end
- describe '#enabled?' do
- subject { client.enabled? }
-
- context 'when terms of service are accepted' do
- it { is_expected.to eq(true) }
-
- context "when private_key isn't present and database is read only" do
- before do
- allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
- end
-
- it 'returns false' do
- expect(::Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(nil)
-
- is_expected.to eq(false)
- end
- end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(pages_auto_ssl: false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
- context 'when terms of service are not accepted' do
- before do
- stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
describe '#terms_of_service_url' do
subject { client.terms_of_service_url }
diff --git a/spec/lib/gitlab/lets_encrypt_spec.rb b/spec/lib/gitlab/lets_encrypt_spec.rb
new file mode 100644
index 00000000000..674b114e9d3
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt do
+ include LetsEncryptHelpers
+
+ before do
+ stub_lets_encrypt_settings
+ end
+
+ describe '.enabled?' do
+ let(:project) { create(:project) }
+ let(:pages_domain) { create(:pages_domain, project: project) }
+
+ subject { described_class.enabled?(pages_domain) }
+
+ context 'when terms of service are accepted' do
+ it { is_expected.to eq(true) }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when terms of service are not accepted' do
+ before do
+ stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when feature flag for project is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl_for_project: false)
+ end
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ end
+ end
+
+ context 'when domain has not project' do
+ let(:pages_domain) { create(:pages_domain) }
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/time_tracking_formatter_spec.rb b/spec/lib/gitlab/time_tracking_formatter_spec.rb
new file mode 100644
index 00000000000..a85d418777f
--- /dev/null
+++ b/spec/lib/gitlab/time_tracking_formatter_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::TimeTrackingFormatter do
+ describe '#parse' do
+ subject { described_class.parse(duration_string) }
+
+ context 'positive durations' do
+ let(:duration_string) { '3h 20m' }
+
+ it { expect(subject).to eq(12_000) }
+ end
+
+ context 'negative durations' do
+ let(:duration_string) { '-3h 20m' }
+
+ it { expect(subject).to eq(-12_000) }
+ end
+ end
+
+ describe '#output' do
+ let(:num_seconds) { 178_800 }
+
+ subject { described_class.output(num_seconds) }
+
+ context 'time_tracking_limit_to_hours setting is true' do
+ before do
+ stub_application_setting(time_tracking_limit_to_hours: true)
+ end
+
+ it { expect(subject).to eq('49h 40m') }
+ end
+
+ context 'time_tracking_limit_to_hours setting is false' do
+ before do
+ stub_application_setting(time_tracking_limit_to_hours: false)
+ end
+
+ it { expect(subject).to eq('1w 1d 1h 40m') }
+ end
+ end
+end
diff --git a/spec/migrations/backport_enterprise_schema_spec.rb b/spec/migrations/backport_enterprise_schema_spec.rb
new file mode 100644
index 00000000000..8d2d9d4953a
--- /dev/null
+++ b/spec/migrations/backport_enterprise_schema_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'migrate', '20190402150158_backport_enterprise_schema.rb')
+
+describe BackportEnterpriseSchema, :migration, schema: 20190329085614 do
+ include MigrationsHelpers
+
+ def drop_if_exists(table)
+ active_record_base.connection.drop_table(table) if active_record_base.connection.table_exists?(table)
+ end
+
+ describe '#up' do
+ it 'creates new EE tables' do
+ migrate!
+
+ expect(active_record_base.connection.table_exists?(:epics)).to be true
+ expect(active_record_base.connection.table_exists?(:geo_nodes)).to be true
+ end
+
+ context 'missing EE columns' do
+ before do
+ drop_if_exists(:epics)
+
+ active_record_base.connection.create_table "epics" do |t|
+ t.integer :group_id, null: false, index: true
+ t.integer :author_id, null: false, index: true
+ end
+ end
+
+ after do
+ drop_if_exists(:epics)
+ end
+
+ it 'flags an error' do
+ expect { migrate! }.to raise_error(/Your database is missing.*that is present for GitLab EE/)
+ end
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index f8dc1541dd3..ab6f6dfe720 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -354,36 +354,6 @@ describe ApplicationSetting do
end
end
- describe 'setting Sentry DSNs' do
- context 'server DSN' do
- it 'strips leading and trailing whitespace' do
- subject.update(sentry_dsn: ' http://test ')
-
- expect(subject.sentry_dsn).to eq('http://test')
- end
-
- it 'handles nil values' do
- subject.update(sentry_dsn: nil)
-
- expect(subject.sentry_dsn).to be_nil
- end
- end
-
- context 'client-side DSN' do
- it 'strips leading and trailing whitespace' do
- subject.update(clientside_sentry_dsn: ' http://test ')
-
- expect(subject.clientside_sentry_dsn).to eq('http://test')
- end
-
- it 'handles nil values' do
- subject.update(clientside_sentry_dsn: nil)
-
- expect(subject.clientside_sentry_dsn).to be_nil
- end
- end
- end
-
describe '#disabled_oauth_sign_in_sources=' do
before do
allow(Devise).to receive(:omniauth_providers).and_return([:github])
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 4d53e4aad8a..020ada3c47a 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -48,14 +48,14 @@ describe BroadcastMessage do
expect(described_class.current).to be_empty
end
- it 'caches the output of the query' do
+ it 'caches the output of the query for two weeks' do
create(:broadcast_message)
- expect(described_class).to receive(:current_and_future_messages).and_call_original.once
+ expect(described_class).to receive(:current_and_future_messages).and_call_original.twice
described_class.current
- Timecop.travel(1.year) do
+ Timecop.travel(3.weeks) do
described_class.current
end
end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 806b4f61bd8..28630f7d3fe 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -158,7 +158,7 @@ describe InternalId do
before do
described_class.reset_column_information
# Project factory will also call the current_version
- expect(ActiveRecord::Migrator).to receive(:current_version).twice.and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
+ expect(ActiveRecord::Migrator).to receive(:current_version).at_least(:once).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
end
it 'does not reset any of the iids' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index fc28c216b21..a2547755510 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2124,7 +2124,7 @@ describe MergeRequest do
allow(subject).to receive(:head_pipeline) { nil }
end
- it { expect(subject.mergeable_ci_state?).to be_truthy }
+ it { expect(subject.mergeable_ci_state?).to be_falsey }
end
end
diff --git a/spec/models/namespace/aggregation_schedule_spec.rb b/spec/models/namespace/aggregation_schedule_spec.rb
new file mode 100644
index 00000000000..5ba7547ff4d
--- /dev/null
+++ b/spec/models/namespace/aggregation_schedule_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespace::AggregationSchedule, type: :model do
+ it { is_expected.to belong_to :namespace }
+end
diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb
new file mode 100644
index 00000000000..f6fb5af5aae
--- /dev/null
+++ b/spec/models/namespace/root_storage_statistics_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespace::RootStorageStatistics, type: :model do
+ it { is_expected.to belong_to :namespace }
+ it { is_expected.to have_one(:route).through(:namespace) }
+
+ it { is_expected.to delegate_method(:all_projects).to(:namespace) }
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index d80183af33e..30e49cf204f 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -15,6 +15,8 @@ describe Namespace do
it { is_expected.to have_many :project_statistics }
it { is_expected.to belong_to :parent }
it { is_expected.to have_many :children }
+ it { is_expected.to have_one :root_storage_statistics }
+ it { is_expected.to have_one :aggregation_schedule }
end
describe 'validations' do
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index ed907841bd8..1c69f5dbb67 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -226,10 +226,8 @@ describe API::Helpers do
allow_any_instance_of(self.class).to receive(:rack_response)
allow(Gitlab::Sentry).to receive(:enabled?).and_return(true)
- stub_application_setting(
- sentry_enabled: true,
- sentry_dsn: "dummy://12345:67890@sentry.localdomain/sentry/42"
- )
+ stub_sentry_settings
+
configure_sentry
Raven.client.configuration.encoding = 'json'
end
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index d5f77f3354b..8d43ce4f662 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -34,8 +34,12 @@ describe PagesDomains::ObtainLetsEncryptCertificateService do
end
context 'when there is no acme order' do
- it 'creates acme order' do
+ it 'creates acme order and schedules next step' do
expect_to_create_acme_challenge
+ expect(PagesDomainSslRenewalWorker).to(
+ receive(:perform_in).with(described_class::CHALLENGE_PROCESSING_DELAY, pages_domain.id)
+ .and_return(nil).once
+ )
service.execute
end
@@ -82,8 +86,12 @@ describe PagesDomains::ObtainLetsEncryptCertificateService do
stub_lets_encrypt_order(existing_order.url, 'ready')
end
- it 'request certificate' do
+ it 'request certificate and schedules next step' do
expect(api_order).to receive(:request_certificate).and_call_original
+ expect(PagesDomainSslRenewalWorker).to(
+ receive(:perform_in).with(described_class::CERTIFICATE_PROCESSING_DELAY, pages_domain.id)
+ .and_return(nil).once
+ )
service.execute
end
diff --git a/spec/services/projects/propagate_service_template_spec.rb b/spec/services/projects/propagate_service_template_spec.rb
index f93e5aae82a..2c3effec617 100644
--- a/spec/services/projects/propagate_service_template_spec.rb
+++ b/spec/services/projects/propagate_service_template_spec.rb
@@ -72,7 +72,7 @@ describe Projects::PropagateServiceTemplate do
expect(project.pushover_service.properties).to eq(service_template.properties)
end
- describe 'bulk update' do
+ describe 'bulk update', :use_sql_query_cache do
let(:project_total) { 5 }
before do
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 2420817e1f7..30a867fa7ba 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -946,6 +946,18 @@ describe SystemNoteService do
expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
end
+
+ context 'when time_tracking_limit_to_hours setting is true' do
+ before do
+ stub_application_setting(time_tracking_limit_to_hours: true)
+ end
+
+ it 'sets the note text' do
+ noteable.update_attribute(:time_estimate, 277200)
+
+ expect(subject.note).to eq "changed time estimate to 77h"
+ end
+ end
end
context 'without a time estimate' do
@@ -1022,6 +1034,18 @@ describe SystemNoteService do
end
end
+ context 'when time_tracking_limit_to_hours setting is true' do
+ before do
+ stub_application_setting(time_tracking_limit_to_hours: true)
+ end
+
+ it 'sets the note text' do
+ spend_time!(277200)
+
+ expect(subject.note).to eq "added 77h of time spent"
+ end
+ end
+
def spend_time!(seconds)
noteable.spend_time(duration: seconds, user_id: author.id)
noteable.save!
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 390a869d93f..3bd2408dc72 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -218,6 +218,12 @@ RSpec.configure do |config|
ActionController::Base.cache_store = caching_store
end
+ config.around(:each, :use_sql_query_cache) do |example|
+ ActiveRecord::Base.cache do
+ example.run
+ end
+ end
+
# The :each scope runs "inside" the example, so this hook ensures the DB is in the
# correct state before any examples' before hooks are called. This prevents a
# problem where `ScheduleIssuesClosedAtTypeChange` (or any migration that depends
diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index 87f825152cf..db662836013 100644
--- a/spec/support/helpers/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -70,6 +70,10 @@ module PrometheusHelpers
WebMock.stub_request(:get, url).to_raise(exception_type)
end
+ def stub_any_prometheus_request
+ WebMock.stub_request(:any, /prometheus.example.com/)
+ end
+
def stub_all_prometheus_requests(environment_slug, body: nil, status: 200)
stub_prometheus_request(
prometheus_query_with_time_url(prometheus_memory_query(environment_slug), Time.now.utc),
diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
index f6c613ad5aa..0d591f038ce 100644
--- a/spec/support/helpers/stub_configuration.rb
+++ b/spec/support/helpers/stub_configuration.rb
@@ -81,6 +81,12 @@ module StubConfiguration
allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages))
end
+ def stub_sentry_settings
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.sentry).to receive(:dsn).and_return('dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/42')
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return('dummy://b44a0828b72421a6d8e99efd68d44fa8@example.com/43')
+ end
+
def stub_kerberos_setting(messages)
allow(Gitlab.config.kerberos).to receive_messages(to_settings(messages))
end
diff --git a/spec/support/inspect_squelch.rb b/spec/support/inspect_squelch.rb
new file mode 100644
index 00000000000..8ee6732370b
--- /dev/null
+++ b/spec/support/inspect_squelch.rb
@@ -0,0 +1,7 @@
+# This class can generate a lot of output if it fails,
+# so squelch the instance variable output.
+class ActiveSupport::Cache::NullStore
+ def inspect
+ "<#{self.class}>"
+ end
+end
diff --git a/spec/support/shared_examples/application_setting_examples.rb b/spec/support/shared_examples/application_setting_examples.rb
index 421303c97be..e7ec24c5b7e 100644
--- a/spec/support/shared_examples/application_setting_examples.rb
+++ b/spec/support/shared_examples/application_setting_examples.rb
@@ -249,43 +249,4 @@ RSpec.shared_examples 'application settings examples' do
expect(setting.password_authentication_enabled_for_web?).to be_falsey
end
-
- describe 'sentry settings' do
- context 'when the sentry settings are not set in gitlab.yml' do
- it 'fallbacks to the settings in the database' do
- setting.sentry_enabled = true
- setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
- setting.clientside_sentry_enabled = true
- setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
-
- allow(Gitlab.config.sentry).to receive(:enabled).and_return(false)
- allow(Gitlab.config.sentry).to receive(:dsn).and_return(nil)
- allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return(nil)
-
- expect(setting.sentry_enabled).to eq true
- expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
- expect(setting.clientside_sentry_enabled).to eq true
- expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
- end
- end
-
- context 'when the sentry settings are set in gitlab.yml' do
- it 'does not fallback to the settings in the database' do
- setting.sentry_enabled = false
- setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
- setting.clientside_sentry_enabled = false
- setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
-
- allow(Gitlab.config.sentry).to receive(:enabled).and_return(true)
- allow(Gitlab.config.sentry).to receive(:dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42')
- allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/43')
-
- expect(setting).not_to receive(:read_attribute)
- expect(setting.sentry_enabled).to eq true
- expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42'
- expect(setting.clientside_sentry_enabled).to eq true
- expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/43'
- end
- end
- end
end
diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb
index 6c4e11910d3..d1a765f27b9 100644
--- a/spec/support/sidekiq.rb
+++ b/spec/support/sidekiq.rb
@@ -30,6 +30,8 @@ RSpec.configure do |config|
end
config.after(:each, :sidekiq, :redis) do
- Sidekiq.redis { |redis| redis.flushdb }
+ Sidekiq.redis do |connection|
+ connection.redis.flushdb
+ end
end
end
diff --git a/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb b/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
index 2ae4872f51d..08a3511f70b 100644
--- a/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_ssl_renewal_cron_worker_spec.rb
@@ -12,15 +12,18 @@ describe PagesDomainSslRenewalCronWorker do
end
describe '#perform' do
- let!(:domain) { create(:pages_domain) }
- let!(:domain_with_enabled_auto_ssl) { create(:pages_domain, auto_ssl_enabled: true) }
- let!(:domain_with_obtained_letsencrypt) { create(:pages_domain, :letsencrypt, auto_ssl_enabled: true) }
+ let(:project) { create :project }
+ let!(:domain) { create(:pages_domain, project: project) }
+ let!(:domain_with_enabled_auto_ssl) { create(:pages_domain, project: project, auto_ssl_enabled: true) }
+ let!(:domain_with_obtained_letsencrypt) do
+ create(:pages_domain, :letsencrypt, project: project, auto_ssl_enabled: true)
+ end
let!(:domain_without_auto_certificate) do
- create(:pages_domain, :without_certificate, :without_key, auto_ssl_enabled: true)
+ create(:pages_domain, :without_certificate, :without_key, project: project, auto_ssl_enabled: true)
end
let!(:domain_with_expired_auto_ssl) do
- create(:pages_domain, :letsencrypt, :with_expired_certificate)
+ create(:pages_domain, :letsencrypt, :with_expired_certificate, project: project)
end
it 'enqueues a PagesDomainSslRenewalWorker for domains needing renewal' do
diff --git a/spec/workers/pages_domain_ssl_renewal_worker_spec.rb b/spec/workers/pages_domain_ssl_renewal_worker_spec.rb
index a3d33de1b40..3552ff0823a 100644
--- a/spec/workers/pages_domain_ssl_renewal_worker_spec.rb
+++ b/spec/workers/pages_domain_ssl_renewal_worker_spec.rb
@@ -7,7 +7,8 @@ describe PagesDomainSslRenewalWorker do
subject(:worker) { described_class.new }
- let(:domain) { create(:pages_domain) }
+ let(:project) { create(:project) }
+ let(:domain) { create(:pages_domain, project: project) }
before do
stub_lets_encrypt_settings
@@ -22,14 +23,24 @@ describe PagesDomainSslRenewalWorker do
worker.perform(domain.id)
end
+ shared_examples 'does nothing' do
+ it 'does nothing' do
+ expect(::PagesDomains::ObtainLetsEncryptCertificateService).not_to receive(:new)
+ end
+ end
+
context 'when domain was deleted' do
before do
domain.destroy!
end
- it 'does nothing' do
- expect(::PagesDomains::ObtainLetsEncryptCertificateService).not_to receive(:new)
- end
+ include_examples 'does nothing'
+ end
+
+ context 'when domain is disabled' do
+ let(:domain) { create(:pages_domain, :disabled) }
+
+ include_examples 'does nothing'
end
end
end
diff --git a/vendor/jupyter/values.yaml b/vendor/jupyter/values.yaml
index 0fbf36b39cc..2aadd3dbe1e 100644
--- a/vendor/jupyter/values.yaml
+++ b/vendor/jupyter/values.yaml
@@ -46,7 +46,7 @@ singleuser:
- "-c"
- >
git clone https://gitlab.com/gitlab-org/nurtch-demo.git DevOps-Runbook-Demo || true;
- echo "https://${GITLAB_USER_LOGIN}:${GITLAB_ACCESS_TOKEN}@${GITLAB_HOST}" > ~/.git-credentials;
+ echo "https://oauth2:${GITLAB_ACCESS_TOKEN}@${GITLAB_HOST}" > ~/.git-credentials;
git config --global credential.helper store;
git config --global user.email "${GITLAB_USER_EMAIL}";
git config --global user.name "${GITLAB_USER_NAME}";
diff --git a/yarn.lock b/yarn.lock
index c54e7cd460b..bbab157d4c7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1686,13 +1686,13 @@ axios-mock-adapter@^1.15.0:
dependencies:
deep-equal "^1.0.1"
-axios@^0.17.1:
- version "0.17.1"
- resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d"
- integrity sha1-LY4+XQvb1zJ/kbyBT1xXZg+Bgk0=
+axios@^0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
+ integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==
dependencies:
- follow-redirects "^1.2.5"
- is-buffer "^1.1.5"
+ follow-redirects "1.5.10"
+ is-buffer "^2.0.2"
babel-code-frame@^6.26.0:
version "6.26.0"
@@ -3410,6 +3410,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
dependencies:
ms "2.0.0"
+debug@=3.1.0, debug@~3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+ dependencies:
+ ms "2.0.0"
+
debug@^3.1.0, debug@^3.2.5, debug@^3.2.6:
version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
@@ -3424,13 +3431,6 @@ debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "^2.1.1"
-debug@~3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
- integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
- dependencies:
- ms "2.0.0"
-
decamelize-keys@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
@@ -4731,12 +4731,12 @@ flush-write-stream@^1.0.0:
inherits "^2.0.1"
readable-stream "^2.0.4"
-follow-redirects@^1.2.5:
- version "1.2.6"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.6.tgz#4dcdc7e4ab3dd6765a97ff89c3b4c258117c79bf"
- integrity sha512-FrMqZ/FONtHnbqO651UPpfRUVukIEwJhXMfdr/JWAmrDbeYBu773b1J6gdWDyRIj4hvvzQEHoEOTrdR8o6KLYA==
+follow-redirects@1.5.10:
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+ integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
dependencies:
- debug "^3.1.0"
+ debug "=3.1.0"
for-in@^1.0.2:
version "1.0.2"
@@ -5734,7 +5734,7 @@ is-buffer@^1.1.5, is-buffer@~1.1.1:
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
-is-buffer@^2.0.0:
+is-buffer@^2.0.0, is-buffer@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"
integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==