summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml10
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock1
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue2
-rw-r--r--app/assets/javascripts/event_tracking/issue_sidebar.js2
-rw-r--r--app/assets/javascripts/issue_show/index.js4
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js8
-rw-r--r--app/assets/javascripts/members.js2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue3
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue334
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue28
-rw-r--r--app/assets/javascripts/monitoring/constants.js9
-rw-r--r--app/assets/javascripts/notes/stores/getters.js39
-rw-r--r--app/assets/javascripts/pages/projects/wikis/wikis.js42
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue2
-rw-r--r--app/assets/javascripts/registry/components/app.vue101
-rw-r--r--app/assets/javascripts/registry/components/svg_message.vue26
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue48
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue83
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue212
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue121
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue96
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue3
-rw-r--r--app/assets/javascripts/users_select.js66
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_modal.vue7
-rw-r--r--app/assets/javascripts/vue_shared/directives/autofocusonshow.js39
-rw-r--r--app/assets/stylesheets/errors.scss2
-rw-r--r--app/assets/stylesheets/framework/badges.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss2
-rw-r--r--app/assets/stylesheets/highlight/common.scss13
-rw-r--r--app/assets/stylesheets/highlight/themes/dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/monokai.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss5
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-dark.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss4
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss18
-rw-r--r--app/assets/stylesheets/pages/container_registry.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss29
-rw-r--r--app/assets/stylesheets/pages/reports.scss5
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/concerns/invisible_captcha.rb4
-rw-r--r--app/controllers/concerns/issuable_collections.rb56
-rw-r--r--app/controllers/concerns/issuable_collections_action.rb2
-rw-r--r--app/controllers/concerns/sorting_preference.rb85
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb19
-rw-r--r--app/controllers/dashboard/projects_controller.rb22
-rw-r--r--app/controllers/explore/projects_controller.rb19
-rw-r--r--app/controllers/projects/blob_controller.rb6
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/controllers/projects/starrers_controller.rb4
-rw-r--r--app/controllers/projects/wikis_controller.rb31
-rw-r--r--app/finders/award_emojis_finder.rb55
-rw-r--r--app/finders/awarded_emoji_finder.rb21
-rw-r--r--app/graphql/mutations/award_emojis/add.rb9
-rw-r--r--app/graphql/mutations/award_emojis/remove.rb15
-rw-r--r--app/graphql/mutations/award_emojis/toggle.rb14
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/mailers/notify.rb15
-rw-r--r--app/models/award_emoji.rb6
-rw-r--r--app/models/concerns/awardable.rb26
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/repository.rb12
-rw-r--r--app/models/users_star_project.rb1
-rw-r--r--app/presenters/blobs/unfold_presenter.rb43
-rw-r--r--app/serializers/deployment_entity.rb2
-rw-r--r--app/serializers/deployment_serializer.rb2
-rw-r--r--app/serializers/issuable_sidebar_basic_entity.rb1
-rw-r--r--app/serializers/merge_request_serializer.rb2
-rw-r--r--app/serializers/merge_request_sidebar_basic_entity.rb9
-rw-r--r--app/services/award_emojis/add_service.rb42
-rw-r--r--app/services/award_emojis/base_service.rb32
-rw-r--r--app/services/award_emojis/collect_user_emoji_service.rb23
-rw-r--r--app/services/award_emojis/destroy_service.rb21
-rw-r--r--app/services/award_emojis/toggle_service.rb13
-rw-r--r--app/services/issuable_base_service.rb5
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/views/admin/applications/_delete_form.html.haml2
-rw-r--r--app/views/doorkeeper/authorized_applications/_delete_form.html.haml2
-rw-r--r--app/views/errors/access_denied.html.haml4
-rw-r--r--app/views/layouts/errors.html.haml6
-rw-r--r--app/views/layouts/header/_default.html.haml7
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml4
-rw-r--r--app/views/projects/commit/_ajax_signature.html.haml2
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml18
-rw-r--r--app/views/projects/wikis/_main_links.html.haml6
-rw-r--r--app/views/projects/wikis/_new.html.haml18
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml11
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml8
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/members/_group.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--changelogs/unreleased/32032-html-code-shown-in-merge-request.yml5
-rw-r--r--changelogs/unreleased/46299-wiki-page-creation.yml5
-rw-r--r--changelogs/unreleased/56130-deployed_at.yml5
-rw-r--r--changelogs/unreleased/59786-show-renamed-file-in-mr.yml5
-rw-r--r--changelogs/unreleased/62373-badge-counter-very-low-contrast-between-foreground-and-background-c.yml6
-rw-r--r--changelogs/unreleased/64251-branch-name-set-cache.yml5
-rw-r--r--changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml5
-rw-r--r--changelogs/unreleased/65412-add-support-for-line-charts.yml5
-rw-r--r--changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml5
-rw-r--r--changelogs/unreleased/66061-update-tooltip-of-detached-label-state.yml5
-rw-r--r--changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml5
-rw-r--r--changelogs/unreleased/66161-replace-expand-icons.yml5
-rw-r--r--changelogs/unreleased/an-fix-sidekiq-histogram-buckets.yml5
-rw-r--r--changelogs/unreleased/bvl-mr-commit-note-counter.yml5
-rw-r--r--changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml5
-rw-r--r--changelogs/unreleased/ci-config-on-policy.yml5
-rw-r--r--changelogs/unreleased/clean-obsolete-css.yml5
-rw-r--r--changelogs/unreleased/fe-fix-merge-url-params-with-plus.yml5
-rw-r--r--changelogs/unreleased/fix-file-row-styling.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml5
-rw-r--r--changelogs/unreleased/pb-update-gitlab-shell-9-4-0.yml5
-rw-r--r--changelogs/unreleased/remove-margin-from-user-header.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issues-api-gitaly-nplusone.yml5
-rw-r--r--changelogs/unreleased/tr-param-undefined-fix.yml5
-rw-r--r--changelogs/unreleased/update-babel-to-7-5-5.yml5
-rw-r--r--config/initializers/rack_attack_logging.rb4
-rw-r--r--config/initializers/sidekiq.rb4
-rw-r--r--config/initializers/zz_metrics.rb6
-rw-r--r--config/prometheus/common_metrics.yml8
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/wiki.rb1
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--danger/only_documentation/Dangerfile2
-rw-r--r--db/fixtures/development/14_pipelines.rb75
-rw-r--r--db/fixtures/development/15_award_emoji.rb35
-rw-r--r--db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb13
-rw-r--r--db/migrate/20190814205640_import_common_metrics_line_charts.rb13
-rw-r--r--db/schema.rb1
-rw-r--r--doc/administration/container_registry.md15
-rw-r--r--doc/administration/geo/disaster_recovery/index.md2
-rw-r--r--doc/administration/geo/replication/index.md4
-rw-r--r--doc/administration/gitaly/index.md82
-rw-r--r--doc/administration/high_availability/load_balancer.md57
-rw-r--r--doc/administration/logs.md2
-rw-r--r--doc/administration/monitoring/gitlab_instance_administration_project/index.md2
-rw-r--r--doc/administration/monitoring/performance/request_profiling.md2
-rw-r--r--doc/administration/troubleshooting/kubernetes_cheat_sheet.md28
-rw-r--r--doc/administration/troubleshooting/sidekiq.md118
-rw-r--r--doc/api/api_resources.md2
-rw-r--r--doc/api/boards.md2
-rw-r--r--doc/api/group_boards.md2
-rw-r--r--doc/api/issues.md4
-rw-r--r--doc/api/merge_request_approvals.md294
-rw-r--r--doc/api/merge_requests.md2
-rw-r--r--doc/ci/directed_acyclic_graph/index.md2
-rw-r--r--doc/ci/docker/using_docker_images.md19
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md5
-rw-r--r--doc/ci/ssh_keys/README.md2
-rw-r--r--doc/ci/variables/README.md7
-rw-r--r--doc/ci/yaml/README.md22
-rw-r--r--doc/development/automatic_ce_ee_merge.md2
-rw-r--r--doc/development/elasticsearch.md43
-rw-r--r--doc/development/fe_guide/axios.md2
-rw-r--r--doc/development/fe_guide/style_guide_js.md522
-rw-r--r--doc/development/fe_guide/vuex.md2
-rw-r--r--doc/development/img/elasticsearch_architecture.svg1
-rw-r--r--doc/development/interacting_components.md4
-rw-r--r--doc/development/new_fe_guide/development/testing.md363
-rw-r--r--doc/development/new_fe_guide/modules/dirty_submit.md9
-rw-r--r--doc/development/new_fe_guide/style/javascript.md2
-rw-r--r--doc/development/query_recorder.md7
-rw-r--r--doc/development/shell_scripting_guide/index.md9
-rw-r--r--doc/development/testing_guide/frontend_testing.md515
-rw-r--r--doc/development/testing_guide/review_apps.md4
-rw-r--r--doc/development/testing_guide/testing_levels.md2
-rw-r--r--doc/gitlab-basics/start-using-git.md21
-rw-r--r--doc/install/azure/index.md2
-rw-r--r--doc/topics/autodevops/index.md49
-rw-r--r--doc/user/group/saml_sso/index.md19
-rw-r--r--doc/user/profile/account/two_factor_authentication.md1
-rw-r--r--doc/user/project/import/gitlab_com.md14
-rw-r--r--doc/user/project/import/img/gitlab_new_project_page.pngbin21251 -> 0 bytes
-rw-r--r--doc/user/project/import/img/gitlab_new_project_page_v12_2.pngbin0 -> 214213 bytes
-rw-r--r--doc/user/project/integrations/img/download_as_csv.pngbin0 -> 33801 bytes
-rw-r--r--doc/user/project/integrations/img/generate_link_to_chart.pngbin0 -> 35573 bytes
-rw-r--r--doc/user/project/integrations/prometheus.md10
-rw-r--r--doc/user/project/members/img/access_requests_management.pngbin11005 -> 10436 bytes
-rw-r--r--doc/user/project/members/img/request_access_button.pngbin25271 -> 27958 bytes
-rw-r--r--doc/user/project/members/img/withdraw_access_request_button.pngbin26123 -> 28154 bytes
-rw-r--r--doc/user/project/merge_requests/fast_forward_merge.md10
-rw-r--r--doc/user/project/merge_requests/img/ff_merge_mr.pngbin0 -> 21380 bytes
-rw-r--r--doc/user/project/merge_requests/img/incrementally_expand_merge_request_diffs_v12_2.pngbin0 -> 96982 bytes
-rw-r--r--doc/user/project/merge_requests/index.md9
-rw-r--r--doc/user/project/repository/img/repository_languages.pngbin26516 -> 0 bytes
-rw-r--r--doc/user/project/repository/img/repository_languages_v12_2.gifbin0 -> 159195 bytes
-rw-r--r--doc/user/project/repository/index.md2
-rw-r--r--doc/user/project/wiki/index.md9
-rw-r--r--doc/workflow/lfs/lfs_administration.md41
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md27
-rw-r--r--doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md2
-rw-r--r--doc/workflow/repository_mirroring.md31
-rw-r--r--doc/workflow/todos.md4
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/award_emoji.rb8
-rw-r--r--lib/api/entities.rb5
-rw-r--r--lib/api/issues.rb6
-rw-r--r--lib/gitlab/action_rate_limiter.rb4
-rw-r--r--lib/gitlab/ci/build/policy/variables.rb2
-rw-r--r--lib/gitlab/ci/build/rules.rb37
-rw-r--r--lib/gitlab/ci/build/rules/rule.rb32
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause.rb31
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/changes.rb23
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/if.rb19
-rw-r--r--lib/gitlab/ci/config/entry/job.rb31
-rw-r--r--lib/gitlab/ci/config/entry/rules.rb33
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule.rb42
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/matches.rb3
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb3
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb67
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml3
-rw-r--r--lib/gitlab/config/entry/validators.rb24
-rw-r--r--lib/gitlab/daemon.rb5
-rw-r--r--lib/gitlab/grape_logging/loggers/client_env_logger.rb16
-rw-r--r--lib/gitlab/repository_cache_adapter.rb53
-rw-r--r--lib/gitlab/repository_set_cache.rb67
-rw-r--r--lib/gitlab/sentry.rb17
-rw-r--r--lib/gitlab/sidekiq_middleware/metrics.rb6
-rw-r--r--lib/gitlab/sidekiq_middleware/monitor.rb16
-rw-r--r--lib/gitlab/sidekiq_monitor.rb182
-rw-r--r--lib/gitlab/usage_data_counters/note_counter.rb8
-rw-r--r--lib/gt_one_coercion.rb7
-rw-r--r--locale/gitlab.pot57
-rw-r--r--package.json8
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb2
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb72
-rw-r--r--spec/controllers/concerns/sorting_preference_spec.rb93
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb8
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb95
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb2
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/health_check_controller_spec.rb12
-rw-r--r--spec/controllers/help_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb6
-rw-r--r--spec/controllers/projects/ci/lints_controller_spec.rb8
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb10
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb4
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb10
-rw-r--r--spec/controllers/projects/cycle_analytics/events_controller_spec.rb6
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb6
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb33
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb8
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb8
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb19
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb18
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb2
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb4
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb4
-rw-r--r--spec/controllers/projects/services_controller_spec.rb2
-rw-r--r--spec/controllers/projects/starrers_controller_spec.rb14
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb77
-rw-r--r--spec/controllers/registrations_controller_spec.rb4
-rw-r--r--spec/controllers/snippets/notes_controller_spec.rb6
-rw-r--r--spec/controllers/users_controller_spec.rb6
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb93
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb68
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb16
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb5
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb30
-rw-r--r--spec/features/signed_commits_spec.rb18
-rw-r--r--spec/finders/award_emojis_finder_spec.rb49
-rw-r--r--spec/fixtures/api/schemas/deployment.json4
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_sidebar.json1
-rw-r--r--spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json422
-rw-r--r--spec/fixtures/security-reports/deprecated/gl-dependency-scanning-report.json178
-rw-r--r--spec/fixtures/security-reports/deprecated/gl-sast-report.json964
-rw-r--r--spec/fixtures/security-reports/feature-branch.zipbin7140 -> 0 bytes
-rw-r--r--spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json16
-rw-r--r--spec/fixtures/security-reports/feature-branch/gl-dast-report.json40
-rw-r--r--spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json181
-rw-r--r--spec/fixtures/security-reports/feature-branch/gl-license-management-report.json42
-rw-r--r--spec/fixtures/security-reports/feature-branch/gl-sast-report.json947
-rw-r--r--spec/fixtures/security-reports/master.zipbin9413 -> 0 bytes
-rw-r--r--spec/fixtures/security-reports/master/gl-container-scanning-report.json105
-rw-r--r--spec/fixtures/security-reports/master/gl-dast-report.json42
-rw-r--r--spec/fixtures/security-reports/master/gl-dependency-scanning-report.json181
-rw-r--r--spec/fixtures/security-reports/master/gl-license-management-report.json817
-rw-r--r--spec/fixtures/security-reports/master/gl-sast-report.json967
-rw-r--r--spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json104
-rw-r--r--spec/fixtures/security-reports/remediations/remediation.patch180
-rw-r--r--spec/fixtures/security-reports/remediations/yarn.lock104
-rw-r--r--spec/frontend/autosave_spec.js11
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js6
-rw-r--r--spec/frontend/mocks/mocks_helper_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js85
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js78
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js189
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js49
-rw-r--r--spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js103
-rw-r--r--spec/frontend/sidebar/user_data_mock.js9
-rw-r--r--spec/frontend/test_setup.js6
-rw-r--r--spec/frontend/wikis_spec.js74
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js2
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js2
-rw-r--r--spec/javascripts/monitoring/charts/time_series_spec.js335
-rw-r--r--spec/javascripts/monitoring/components/dashboard_spec.js (renamed from spec/javascripts/monitoring/dashboard_spec.js)22
-rw-r--r--spec/javascripts/monitoring/mock_data.js4
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js130
-rw-r--r--spec/javascripts/registry/components/app_spec.js11
-rw-r--r--spec/javascripts/sidebar/assignee_title_spec.js14
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js206
-rw-r--r--spec/javascripts/sidebar/confidential_issue_sidebar_spec.js8
-rw-r--r--spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js8
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js9
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js18
-rw-r--r--spec/javascripts/vue_shared/directives/autofocusonshow_spec.js38
-rw-r--r--spec/lib/gitlab/action_rate_limiter_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule_spec.rb50
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb168
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb111
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb208
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules_spec.rb135
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb287
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb114
-rw-r--r--spec/lib/gitlab/daemon_spec.rb30
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb7
-rw-r--r--spec/lib/gitlab/repository_set_cache_spec.rb73
-rw-r--r--spec/lib/gitlab/sentry_spec.rb29
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb41
-rw-r--r--spec/lib/gitlab/sidekiq_monitor_spec.rb261
-rw-r--r--spec/lib/gitlab/usage_data_counters/note_counter_spec.rb24
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb10
-rw-r--r--spec/mailers/notify_spec.rb67
-rw-r--r--spec/models/award_emoji_spec.rb23
-rw-r--r--spec/models/concerns/awardable_spec.rb10
-rw-r--r--spec/models/repository_spec.rb52
-rw-r--r--spec/presenters/blobs/unfold_presenter_spec.rb25
-rw-r--r--spec/requests/api/award_emoji_spec.rb16
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/add_spec.rb17
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb17
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb8
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb1
-rw-r--r--spec/requests/api/issues/issues_spec.rb4
-rw-r--r--spec/requests/rack_attack_global_spec.rb12
-rw-r--r--spec/serializers/deployment_entity_spec.rb4
-rw-r--r--spec/serializers/merge_request_sidebar_basic_entity_spec.rb22
-rw-r--r--spec/services/award_emojis/add_service_spec.rb103
-rw-r--r--spec/services/award_emojis/collect_user_emoji_service_spec.rb (renamed from spec/finders/awarded_emoji_finder_spec.rb)2
-rw-r--r--spec/services/award_emojis/destroy_service_spec.rb89
-rw-r--r--spec/services/award_emojis/toggle_service_spec.rb72
-rw-r--r--spec/services/issues/update_service_spec.rb4
-rw-r--r--spec/services/projects/create_service_spec.rb1
-rw-r--r--spec/services/system_note_service_spec.rb4
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb4
-rw-r--r--spec/support/shared_examples/award_emoji_todo_shared_examples.rb59
-rw-r--r--spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb6
-rw-r--r--yarn.lock36
370 files changed, 7868 insertions, 7695 deletions
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index ca55bbd32a7..fc0e93ee9f1 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -2,6 +2,7 @@ include:
- template: Code-Quality.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml
+ - template: Security/DAST.gitlab-ci.yml
code_quality:
extends: .dedicated-no-docs
@@ -24,3 +25,12 @@ dependency_scanning:
tags: []
before_script: []
cache: {}
+
+dast:
+ extends: .dedicated-no-docs
+ stage: qa
+ dependencies:
+ - review-deploy
+ before_script:
+ - export DAST_WEBSITE="$(cat review_app_url.txt)"
+ cache: {}
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index b13d146a7b0..ccfb75e5120 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-9.3.0
+9.4.1
diff --git a/Gemfile b/Gemfile
index 59aa67e642e..116c94bdaf8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -275,7 +275,6 @@ gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3'
gem 'gon', '~> 6.2'
gem 'request_store', '~> 1.3'
-gem 'virtus', '~> 1.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
diff --git a/Gemfile.lock b/Gemfile.lock
index d8f7ca994da..0ecf3aa2840 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1252,7 +1252,6 @@ DEPENDENCIES
unicorn-worker-killer (~> 0.4.4)
validates_hostname (~> 1.0.6)
version_sorter (~> 2.2.4)
- virtus (~> 1.0.1)
vmstat (~> 2.3.0)
webmock (~> 3.5.1)
webpack-rails (~> 0.9.10)
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 925385fa98a..839ab542377 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -212,19 +212,18 @@ export default {
</script>
<template>
- <td :colspan="colspan">
+ <td :colspan="colspan" class="text-center">
<div class="content js-line-expansion-content">
<a
v-if="canExpandUp"
v-tooltip
- class="cursor-pointer js-unfold unfold-icon"
+ class="cursor-pointer js-unfold unfold-icon d-inline-block pt-2 pb-2"
data-placement="top"
data-container="body"
:title="__('Expand up')"
@click="handleExpandLines(EXPAND_UP)"
>
- <!-- TODO: remove style & replace with correct icon, waiting for MR https://gitlab.com/gitlab-org/gitlab-design/issues/499 -->
- <icon :size="12" name="expand-left" aria-hidden="true" style="transform: rotate(270deg);" />
+ <icon :size="12" name="expand-up" aria-hidden="true" />
</a>
<a class="mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()">
<span>{{ s__('Diffs|Show all lines') }}</span>
@@ -232,14 +231,13 @@ export default {
<a
v-if="canExpandDown"
v-tooltip
- class="cursor-pointer js-unfold-down has-tooltip unfold-icon"
+ class="cursor-pointer js-unfold-down has-tooltip unfold-icon d-inline-block pt-2 pb-2"
data-placement="top"
data-container="body"
:title="__('Expand down')"
@click="handleExpandLines(EXPAND_DOWN)"
>
- <!-- TODO: remove style & replace with correct icon, waiting for MR https://gitlab.com/gitlab-org/gitlab-design/issues/499 -->
- <icon :size="12" name="expand-left" aria-hidden="true" style="transform: rotate(90deg);" />
+ <icon :size="12" name="expand-down" aria-hidden="true" />
</a>
</div>
</td>
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 32fbeaaa905..69ec6ab8600 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -130,7 +130,7 @@ export default {
return `\`${this.diffFile.file_path}\``;
},
isFileRenamed() {
- return this.diffFile.viewer.name === diffViewerModes.renamed;
+ return this.diffFile.renamed_file;
},
isModeChanged() {
return this.diffFile.viewer.name === diffViewerModes.mode_changed;
diff --git a/app/assets/javascripts/event_tracking/issue_sidebar.js b/app/assets/javascripts/event_tracking/issue_sidebar.js
new file mode 100644
index 00000000000..6909f82c66f
--- /dev/null
+++ b/app/assets/javascripts/event_tracking/issue_sidebar.js
@@ -0,0 +1,2 @@
+export const initSidebarTracking = () => {};
+export const trackEvent = () => {};
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index 529b6386221..5a9dd91817e 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { initSidebarTracking } from 'ee_else_ce/event_tracking/issue_sidebar';
import issuableApp from './components/app.vue';
import { parseIssuableData } from './utils/parse_data';
import '../vue_shared/vue_resource_interceptor';
@@ -9,6 +10,9 @@ export default function initIssueableApp() {
components: {
issuableApp,
},
+ mounted() {
+ initSidebarTracking();
+ },
render(createElement) {
return createElement('issuable-app', {
props: parseIssuableData(),
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 1336b6a5461..7ead9d46fbb 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,5 +1,11 @@
import { join as joinPaths } from 'path';
+// Returns a decoded url parameter value
+// - Treats '+' as '%20'
+function decodeUrlParameter(val) {
+ return decodeURIComponent(val.replace(/\+/g, '%20'));
+}
+
// Returns an array containing the value(s) of the
// of the key passed as an argument
export function getParameterValues(sParam, url = window.location) {
@@ -30,7 +36,7 @@ export function mergeUrlParams(params, url) {
.forEach(part => {
if (part.length) {
const kv = part.split('=');
- merged[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('='));
+ merged[decodeUrlParameter(kv[0])] = decodeUrlParameter(kv.slice(1).join('='));
}
});
}
diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js
index af2697444f2..d719fd8748d 100644
--- a/app/assets/javascripts/members.js
+++ b/app/assets/javascripts/members.js
@@ -17,6 +17,8 @@ export default class Members {
}
dropdownClicked(options) {
+ options.e.preventDefault();
+
this.formSubmit(null, options.$el);
}
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 90c764587a3..78d97a3c122 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -12,6 +12,9 @@ import { graphDataValidatorForValues } from '../../utils';
let debouncedResize;
+// TODO: Remove this component in favor of the more general time_series.vue
+// Please port all changes here to time_series.vue as well.
+
export default {
components: {
GlAreaChart,
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
new file mode 100644
index 00000000000..2fdc75f63ca
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -0,0 +1,334 @@
+<script>
+import { __ } from '~/locale';
+import { mapState } from 'vuex';
+import { GlLink, GlButton } from '@gitlab/ui';
+import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
+import dateFormat from 'dateformat';
+import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import Icon from '~/vue_shared/components/icon.vue';
+import { chartHeight, graphTypes, lineTypes, symbolSizes, dateFormats } from '../../constants';
+import { makeDataSeries } from '~/helpers/monitor_helper';
+import { graphDataValidatorForValues } from '../../utils';
+
+let debouncedResize;
+
+export default {
+ components: {
+ GlAreaChart,
+ GlLineChart,
+ GlButton,
+ GlChartSeriesLabel,
+ GlLink,
+ Icon,
+ },
+ inheritAttrs: false,
+ props: {
+ graphData: {
+ type: Object,
+ required: true,
+ validator: graphDataValidatorForValues.bind(null, false),
+ },
+ containerWidth: {
+ type: Number,
+ required: true,
+ },
+ deploymentData: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showBorder: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ thresholds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ tooltip: {
+ title: '',
+ content: [],
+ commitUrl: '',
+ isDeployment: false,
+ sha: '',
+ },
+ width: 0,
+ height: chartHeight,
+ svgs: {},
+ primaryColor: null,
+ };
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']),
+ chartData() {
+ // Transforms & supplements query data to render appropriate labels & styles
+ // Input: [{ queryAttributes1 }, { queryAttributes2 }]
+ // Output: [{ seriesAttributes1 }, { seriesAttributes2 }]
+ return this.graphData.queries.reduce((acc, query) => {
+ const { appearance } = query;
+ const lineType =
+ appearance && appearance.line && appearance.line.type
+ ? appearance.line.type
+ : lineTypes.default;
+ const lineWidth =
+ appearance && appearance.line && appearance.line.width
+ ? appearance.line.width
+ : undefined;
+ const areaStyle = {
+ opacity:
+ appearance && appearance.area && typeof appearance.area.opacity === 'number'
+ ? appearance.area.opacity
+ : undefined,
+ };
+
+ const series = makeDataSeries(query.result, {
+ name: this.formatLegendLabel(query),
+ lineStyle: {
+ type: lineType,
+ width: lineWidth,
+ },
+ showSymbol: false,
+ areaStyle: this.graphData.type === 'area-chart' ? areaStyle : undefined,
+ });
+
+ return acc.concat(series);
+ }, []);
+ },
+ chartOptions() {
+ return {
+ xAxis: {
+ name: __('Time'),
+ type: 'time',
+ axisLabel: {
+ formatter: date => dateFormat(date, dateFormats.timeOfDay),
+ },
+ axisPointer: {
+ snap: true,
+ },
+ },
+ yAxis: {
+ name: this.yAxisLabel,
+ axisLabel: {
+ formatter: num => roundOffFloat(num, 3).toString(),
+ },
+ },
+ series: this.scatterSeries,
+ dataZoom: this.dataZoomConfig,
+ };
+ },
+ dataZoomConfig() {
+ const handleIcon = this.svgs['scroll-handle'];
+
+ return handleIcon ? { handleIcon } : {};
+ },
+ earliestDatapoint() {
+ return this.chartData.reduce((acc, series) => {
+ const { data } = series;
+ const { length } = data;
+ if (!length) {
+ return acc;
+ }
+
+ const [first] = data[0];
+ const [last] = data[length - 1];
+ const seriesEarliest = first < last ? first : last;
+
+ return seriesEarliest < acc || acc === null ? seriesEarliest : acc;
+ }, null);
+ },
+ glChartComponent() {
+ const chartTypes = {
+ 'area-chart': GlAreaChart,
+ 'line-chart': GlLineChart,
+ };
+ return chartTypes[this.graphData.type] || GlAreaChart;
+ },
+ isMultiSeries() {
+ return this.tooltip.content.length > 1;
+ },
+ recentDeployments() {
+ return this.deploymentData.reduce((acc, deployment) => {
+ if (deployment.created_at >= this.earliestDatapoint) {
+ const { id, created_at, sha, ref, tag } = deployment;
+ acc.push({
+ id,
+ createdAt: created_at,
+ sha,
+ commitUrl: `${this.projectPath}/commit/${sha}`,
+ tag,
+ tagUrl: tag ? `${this.tagsPath}/${ref.name}` : null,
+ ref: ref.name,
+ showDeploymentFlag: false,
+ });
+ }
+
+ return acc;
+ }, []);
+ },
+ scatterSeries() {
+ return {
+ type: graphTypes.deploymentData,
+ data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]),
+ symbol: this.svgs.rocket,
+ symbolSize: symbolSizes.default,
+ itemStyle: {
+ color: this.primaryColor,
+ },
+ };
+ },
+ yAxisLabel() {
+ return `${this.graphData.y_label}`;
+ },
+ csvText() {
+ const chartData = this.chartData[0].data;
+ const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
+ return chartData.reduce((csv, data) => {
+ const row = data.join(',');
+ return `${csv}${row}\r\n`;
+ }, header);
+ },
+ downloadLink() {
+ const data = new Blob([this.csvText], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
+ },
+ watch: {
+ containerWidth: 'onResize',
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', debouncedResize);
+ },
+ created() {
+ debouncedResize = debounceByAnimationFrame(this.onResize);
+ window.addEventListener('resize', debouncedResize);
+ this.setSvg('rocket');
+ this.setSvg('scroll-handle');
+ },
+ methods: {
+ formatLegendLabel(query) {
+ return `${query.label}`;
+ },
+ formatTooltipText(params) {
+ this.tooltip.title = dateFormat(params.value, dateFormats.default);
+ this.tooltip.content = [];
+ params.seriesData.forEach(dataPoint => {
+ const [xVal, yVal] = dataPoint.value;
+ this.tooltip.isDeployment = dataPoint.componentSubType === graphTypes.deploymentData;
+ if (this.tooltip.isDeployment) {
+ const [deploy] = this.recentDeployments.filter(
+ deployment => deployment.createdAt === xVal,
+ );
+ this.tooltip.sha = deploy.sha.substring(0, 8);
+ this.tooltip.commitUrl = deploy.commitUrl;
+ } else {
+ const { seriesName, color } = dataPoint;
+ const value = yVal.toFixed(3);
+ this.tooltip.content.push({
+ name: seriesName,
+ value,
+ color,
+ });
+ }
+ });
+ },
+ setSvg(name) {
+ getSvgIconPathContent(name)
+ .then(path => {
+ if (path) {
+ this.$set(this.svgs, name, `path://${path}`);
+ }
+ })
+ .catch(e => {
+ // eslint-disable-next-line no-console, @gitlab/i18n/no-non-i18n-strings
+ console.error('SVG could not be rendered correctly: ', e);
+ });
+ },
+ onChartUpdated(chart) {
+ [this.primaryColor] = chart.getOption().color;
+ },
+ onResize() {
+ if (!this.$refs.chart) return;
+ const { width } = this.$refs.chart.$el.getBoundingClientRect();
+ this.width = width;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']">
+ <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
+ <div class="prometheus-graph-header">
+ <h5 class="prometheus-graph-title js-graph-title">{{ graphData.title }}</h5>
+ <gl-button
+ v-if="exportMetricsToCsvEnabled"
+ :href="downloadLink"
+ :title="__('Download CSV')"
+ :aria-label="__('Download CSV')"
+ style="margin-left: 200px;"
+ download="chart_metrics.csv"
+ >
+ {{ __('Download CSV') }}
+ </gl-button>
+ <div class="prometheus-graph-widgets js-graph-widgets">
+ <slot></slot>
+ </div>
+ </div>
+
+ <component
+ :is="glChartComponent"
+ ref="chart"
+ v-bind="$attrs"
+ :data="chartData"
+ :option="chartOptions"
+ :format-tooltip-text="formatTooltipText"
+ :thresholds="thresholds"
+ :width="width"
+ :height="height"
+ @updated="onChartUpdated"
+ >
+ <template v-if="tooltip.isDeployment">
+ <template slot="tooltipTitle">
+ {{ __('Deployed') }}
+ </template>
+ <div slot="tooltipContent" class="d-flex align-items-center">
+ <icon name="commit" class="mr-2" />
+ <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
+ </div>
+ </template>
+ <template v-else>
+ <template slot="tooltipTitle">
+ <div class="text-nowrap">
+ {{ tooltip.title }}
+ </div>
+ </template>
+ <template slot="tooltipContent">
+ <div
+ v-for="(content, key) in tooltip.content"
+ :key="key"
+ class="d-flex justify-content-between"
+ >
+ <gl-chart-series-label :color="isMultiSeries ? content.color : ''">
+ {{ content.name }}
+ </gl-chart-series-label>
+ <div class="prepend-left-32">
+ {{ content.value }}
+ </div>
+ </div>
+ </template>
+ </template>
+ </component>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index dfeeba238ca..d330ceb836c 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -14,9 +14,9 @@ import { __, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
-import MonitorAreaChart from './charts/area.vue';
+import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
-import PanelType from './panel_type.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import { sidebarAnimationDuration, timeWindows } from '../constants';
@@ -26,7 +26,7 @@ let sidebarMutationObserver;
export default {
components: {
- MonitorAreaChart,
+ MonitorTimeSeriesChart,
MonitorSingleStatChart,
PanelType,
GraphGroup,
@@ -141,6 +141,16 @@ export default {
required: false,
default: false,
},
+ alertsEndpoint: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ prometheusAlertsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -264,12 +274,12 @@ export default {
showToast() {
this.$toast.show(__('Link copied to clipboard'));
},
+ // TODO: END
generateLink(group, title, yLabel) {
const dashboard = this.currentDashboard || this.firstDashboard.path;
- const params = { dashboard, group, title, y_label: yLabel };
+ const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null);
return mergeUrlParams(params, window.location.href);
},
- // TODO: END
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
},
@@ -449,11 +459,13 @@ export default {
:clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
:graph-data="graphData"
:dashboard-width="elWidth"
+ :alerts-endpoint="alertsEndpoint"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
:index="`${index}-${graphIndex}`"
/>
</template>
<template v-else>
- <monitor-area-chart
+ <monitor-time-series-chart
v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="graphIndex"
:graph-data="graphData"
@@ -461,7 +473,7 @@ export default {
:thresholds="getGraphAlertValues(graphData.queries)"
:container-width="elWidth"
:project-path="projectPath"
- group-id="monitor-area-chart"
+ group-id="monitor-time-series-chart"
>
<div class="d-flex align-items-center">
<alert-widget
@@ -503,7 +515,7 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</div>
- </monitor-area-chart>
+ </monitor-time-series-chart>
</template>
</graph-group>
</div>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index d7d89522732..13aba3d9f44 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -8,6 +8,10 @@ export const graphTypes = {
deploymentData: 'scatter',
};
+export const symbolSizes = {
+ default: 14,
+};
+
export const lineTypes = {
default: 'solid',
};
@@ -21,6 +25,11 @@ export const timeWindows = {
oneWeek: __('1 week'),
};
+export const dateFormats = {
+ timeOfDay: 'h:MM TT',
+ default: 'dd mmm yyyy, h:MMTT',
+};
+
export const secondsIn = {
thirtyMinutes: 60 * 30,
threeHours: 60 * 60 * 3,
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 52410f18d4a..3d0ec8cd3a7 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -171,26 +171,33 @@ export const isLastUnresolvedDiscussion = (state, getters) => (discussionId, dif
return lastDiscussionId === discussionId;
};
-// Gets the ID of the discussion following the one provided, respecting order (diff or date)
-// @param {Boolean} discussionId - id of the current discussion
-// @param {Boolean} diffOrder - is ordered by diff?
-export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
- const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
- const currentIndex = idsOrdered.indexOf(discussionId);
- const slicedIds = idsOrdered.slice(currentIndex + 1, currentIndex + 2);
+export const findUnresolvedDiscussionIdNeighbor = (state, getters) => ({
+ discussionId,
+ diffOrder,
+ step,
+}) => {
+ const ids = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
+ const index = ids.indexOf(discussionId) + step;
+
+ if (index < 0 && step < 0) {
+ return ids[ids.length - 1];
+ }
+
+ if (index === ids.length && step > 0) {
+ return ids[0];
+ }
- // Get the first ID if there is none after the currentIndex
- return slicedIds.length ? idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0] : idsOrdered[0];
+ return ids[index];
};
-export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => {
- const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder);
- const currentIndex = idsOrdered.indexOf(discussionId);
- const slicedIds = idsOrdered.slice(currentIndex - 1, currentIndex);
+// Gets the ID of the discussion following the one provided, respecting order (diff or date)
+// @param {Boolean} discussionId - id of the current discussion
+// @param {Boolean} diffOrder - is ordered by diff?
+export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) =>
+ getters.findUnresolvedDiscussionIdNeighbor({ discussionId, diffOrder, step: 1 });
- // Get the last ID if there is none after the currentIndex
- return slicedIds.length ? slicedIds[0] : idsOrdered[idsOrdered.length - 1];
-};
+export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) =>
+ getters.findUnresolvedDiscussionIdNeighbor({ discussionId, diffOrder, step: -1 });
// @param {Boolean} diffOrder - is ordered by diff?
export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index 9b58d42b47d..d41199f6374 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -1,6 +1,5 @@
import bp from '../../../breakpoints';
-import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
-import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
+import { s__, sprintf } from '~/locale';
export default class Wikis {
constructor() {
@@ -12,32 +11,37 @@ export default class Wikis {
sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
}
- this.newWikiForm = document.querySelector('form.new-wiki-page');
- if (this.newWikiForm) {
- this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
+ this.isNewWikiPage = Boolean(document.querySelector('.js-new-wiki-page'));
+ this.editTitleInput = document.querySelector('form.wiki-form #wiki_title');
+ this.commitMessageInput = document.querySelector('form.wiki-form #wiki_message');
+ this.commitMessageI18n = this.isNewWikiPage
+ ? s__('WikiPageCreate|Create %{pageTitle}')
+ : s__('WikiPageEdit|Update %{pageTitle}');
+
+ if (this.editTitleInput) {
+ // Initialize the commit message on load
+ if (this.editTitleInput.value) this.setWikiCommitMessage(this.editTitleInput.value);
+
+ // Set the commit message as the page title is changed
+ this.editTitleInput.addEventListener('keyup', e => this.handleWikiTitleChange(e));
}
window.addEventListener('resize', () => this.renderSidebar());
this.renderSidebar();
}
- handleNewWikiSubmit(e) {
- if (!this.newWikiForm) return;
-
- const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
-
- const slug = slugInput.value;
+ handleWikiTitleChange(e) {
+ this.setWikiCommitMessage(e.target.value);
+ }
- if (slug.length > 0) {
- const wikisPath = slugInput.getAttribute('data-wikis-path');
+ setWikiCommitMessage(rawTitle) {
+ let title = rawTitle;
- // If the wiki is empty, we need to merge the current URL params to keep the "create" view.
- const params = parseQueryStringIntoObject(window.location.search.substr(1));
- const url = mergeUrlParams(params, `${wikisPath}/${slug}`);
- redirectTo(url);
+ // Replace hyphens with spaces
+ if (title) title = title.replace(/-+/g, ' ');
- e.preventDefault();
- }
+ const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: title });
+ this.commitMessageInput.value = newCommitMessage;
}
handleToggleSidebar(e) {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index e01080b04d6..4efc1d2408a 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -104,7 +104,7 @@ export default {
v-gl-tooltip
:title="
__(
- 'The code of a detached pipeline is tested against the source branch instead of merged results',
+ 'Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results.',
)
"
class="js-pipeline-url-detached badge badge-info"
diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue
index efbf0a4e3cf..346dc470a59 100644
--- a/app/assets/javascripts/registry/components/app.vue
+++ b/app/assets/javascripts/registry/components/app.vue
@@ -1,10 +1,9 @@
<script>
import { mapGetters, mapActions } from 'vuex';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
import store from '../stores';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import CollapsibleContainer from './collapsible_container.vue';
-import SvgMessage from './svg_message.vue';
import { s__, sprintf } from '../../locale';
export default {
@@ -12,8 +11,8 @@ export default {
components: {
clipboardButton,
CollapsibleContainer,
+ GlEmptyState,
GlLoadingIcon,
- SvgMessage,
},
props: {
endpoint: {
@@ -93,7 +92,9 @@ export default {
this.setMainEndpoint(this.endpoint);
},
mounted() {
- this.fetchRepos();
+ if (!this.characterError) {
+ this.fetchRepos();
+ }
},
methods: {
...mapActions(['setMainEndpoint', 'fetchRepos']),
@@ -102,61 +103,63 @@ export default {
</script>
<template>
<div>
- <svg-message v-if="characterError" id="invalid-characters" :svg-path="containersErrorImage">
- <h4>
- {{ s__('ContainerRegistry|Docker connection error') }}
- </h4>
- <p v-html="dockerConnectionErrorText"></p>
- </svg-message>
+ <gl-empty-state
+ v-if="characterError"
+ :title="s__('ContainerRegistry|Docker connection error')"
+ :svg-path="containersErrorImage"
+ >
+ <template #description>
+ <p v-html="dockerConnectionErrorText"></p>
+ </template>
+ </gl-empty-state>
- <gl-loading-icon v-else-if="isLoading && !characterError" size="md" class="prepend-top-16" />
+ <gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" />
- <div v-else-if="!isLoading && !characterError && repos.length">
+ <div v-else-if="!isLoading && repos.length">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<p v-html="introText"></p>
<collapsible-container v-for="item in repos" :key="item.id" :repo="item" />
</div>
- <svg-message
- v-else-if="!isLoading && !characterError && !repos.length"
- id="no-container-images"
+ <gl-empty-state
+ v-else
+ :title="s__('ContainerRegistry|There are no container images stored for this project')"
:svg-path="noContainersImage"
+ class="container-message"
>
- <h4>
- {{ s__('ContainerRegistry|There are no container images stored for this project') }}
- </h4>
- <p v-html="noContainerImagesText"></p>
-
- <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
- <p>
- {{
- s__(
- 'ContainerRegistry|You can add an image to this registry with the following commands:',
- )
- }}
- </p>
+ <template #description>
+ <p class="js-no-container-images-text" v-html="noContainerImagesText"></p>
+ <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
+ <p>
+ {{
+ s__(
+ 'ContainerRegistry|You can add an image to this registry with the following commands:',
+ )
+ }}
+ </p>
- <div class="input-group append-bottom-10">
- <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
- <span class="input-group-append">
- <clipboard-button
- :text="dockerBuildCommand"
- :title="s__('ContainerRegistry|Copy build command to clipboard')"
- class="input-group-text"
- />
- </span>
- </div>
+ <div class="input-group append-bottom-10">
+ <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerBuildCommand"
+ :title="s__('ContainerRegistry|Copy build command to clipboard')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
- <div class="input-group">
- <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
- <span class="input-group-append">
- <clipboard-button
- :text="dockerPushCommand"
- :title="s__('ContainerRegistry|Copy push command to clipboard')"
- class="input-group-text"
- />
- </span>
- </div>
- </svg-message>
+ <div class="input-group">
+ <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly />
+ <span class="input-group-append">
+ <clipboard-button
+ :text="dockerPushCommand"
+ :title="s__('ContainerRegistry|Copy push command to clipboard')"
+ class="input-group-text"
+ />
+ </span>
+ </div>
+ </template>
+ </gl-empty-state>
</div>
</template>
diff --git a/app/assets/javascripts/registry/components/svg_message.vue b/app/assets/javascripts/registry/components/svg_message.vue
deleted file mode 100644
index 617093e054e..00000000000
--- a/app/assets/javascripts/registry/components/svg_message.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-export default {
- name: 'RegistrySvgMessage',
- props: {
- id: {
- type: String,
- required: true,
- },
- svgPath: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div :id="id" class="empty-state container-message">
- <div class="svg-content">
- <img :src="svgPath" class="flex-align-self-center" />
- </div>
- <div class="text-content">
- <slot></slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
new file mode 100644
index 00000000000..71a1fc31315
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue
@@ -0,0 +1,48 @@
+<script>
+import { __, sprintf } from '~/locale';
+
+export default {
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ imgSize: {
+ type: Number,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ computed: {
+ assigneeAlt() {
+ return sprintf(__("%{userName}'s avatar"), { userName: this.user.name });
+ },
+ avatarUrl() {
+ return this.user.avatar || this.user.avatar_url || gon.default_avatar_url;
+ },
+ isMergeRequest() {
+ return this.issuableType === 'merge_request';
+ },
+ hasMergeIcon() {
+ return this.isMergeRequest && !this.user.can_merge;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="position-relative">
+ <img
+ :alt="assigneeAlt"
+ :src="avatarUrl"
+ :width="imgSize"
+ :class="`s${imgSize}`"
+ class="avatar avatar-inline m-0"
+ />
+ <i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i>
+ </span>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
new file mode 100644
index 00000000000..6633a63d046
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue
@@ -0,0 +1,83 @@
+<script>
+import { __, sprintf } from '~/locale';
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
+import { joinPaths } from '~/lib/utils/url_utility';
+import AssigneeAvatar from './assignee_avatar.vue';
+
+export default {
+ components: {
+ AssigneeAvatar,
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ tooltipPlacement: {
+ type: String,
+ default: 'bottom',
+ required: false,
+ },
+ tooltipHasName: {
+ type: Boolean,
+ default: true,
+ required: false,
+ },
+ issuableType: {
+ type: String,
+ default: 'issue',
+ required: false,
+ },
+ },
+ computed: {
+ cannotMerge() {
+ return this.issuableType === 'merge_request' && !this.user.can_merge;
+ },
+ tooltipTitle() {
+ if (this.cannotMerge && this.tooltipHasName) {
+ return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name });
+ } else if (this.cannotMerge) {
+ return __('Cannot merge');
+ } else if (this.tooltipHasName) {
+ return this.user.name;
+ }
+
+ return '';
+ },
+ tooltipOption() {
+ return {
+ container: 'body',
+ placement: this.tooltipPlacement,
+ boundary: 'viewport',
+ };
+ },
+ assigneeUrl() {
+ return joinPaths(`${this.rootPath}`, `${this.user.username}`);
+ },
+ },
+};
+</script>
+
+<template>
+ <!-- must be `d-inline-block` or parent flex-basis causes width issues -->
+ <gl-link
+ v-gl-tooltip="tooltipOption"
+ :href="assigneeUrl"
+ :title="tooltipTitle"
+ class="d-inline-block"
+ >
+ <!-- use d-flex so that slot can be appropriately styled -->
+ <span class="d-flex">
+ <assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" />
+ <slot :user="user"></slot>
+ </span>
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index fa6b6bfaef1..63b93a80ead 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -1,5 +1,6 @@
<script>
import { n__ } from '~/locale';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
name: 'AssigneeTitle',
@@ -29,13 +30,23 @@ export default {
return n__('Assignee', `%d Assignees`, assignees);
},
},
+ methods: {
+ trackEdit() {
+ trackEvent('click_edit_button', 'assignee');
+ },
+ },
};
</script>
<template>
<div class="title hide-collapsed">
{{ assigneeTitle }}
<i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"></i>
- <a v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" href="#">
+ <a
+ v-if="editable"
+ class="js-sidebar-dropdown-toggle edit-link float-right"
+ href="#"
+ @click.prevent="trackEdit"
+ >
{{ __('Edit') }}
</a>
<a
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 631e2e28d4d..d9739e8d197 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,13 +1,14 @@
<script>
-import { __, sprintf } from '~/locale';
-import tooltip from '~/vue_shared/directives/tooltip';
+import CollapsedAssigneeList from '../assignees/collapsed_assignee_list.vue';
+import UncollapsedAssigneeList from '../assignees/uncollapsed_assignee_list.vue';
export default {
// name: 'Assignees' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
name: 'Assignees',
- directives: {
- tooltip,
+ components: {
+ CollapsedAssigneeList,
+ UncollapsedAssigneeList,
},
props: {
rootPath: {
@@ -24,171 +25,34 @@ export default {
},
issuableType: {
type: String,
- require: true,
+ required: false,
default: 'issue',
},
},
- data() {
- return {
- defaultRenderCount: 5,
- defaultMaxCounter: 99,
- showLess: true,
- };
- },
computed: {
- firstUser() {
- return this.users[0];
- },
- hasMoreThanTwoAssignees() {
- return this.users.length > 2;
- },
- hasMoreThanOneAssignee() {
- return this.users.length > 1;
- },
- hasAssignees() {
- return this.users.length > 0;
- },
hasNoUsers() {
return !this.users.length;
},
- hasOneUser() {
- return this.users.length === 1;
- },
- renderShowMoreSection() {
- return this.users.length > this.defaultRenderCount;
- },
- numberOfHiddenAssignees() {
- return this.users.length - this.defaultRenderCount;
- },
- isHiddenAssignees() {
- return this.numberOfHiddenAssignees > 0;
- },
- hiddenAssigneesLabel() {
- const { numberOfHiddenAssignees } = this;
- return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees });
- },
- collapsedTooltipTitle() {
- const maxRender = Math.min(this.defaultRenderCount, this.users.length);
- const renderUsers = this.users.slice(0, maxRender);
- const names = renderUsers.map(u => u.name);
-
- if (this.users.length > maxRender) {
- names.push(`+ ${this.users.length - maxRender} more`);
- }
-
- if (!this.users.length) {
- const emptyTooltipLabel = __('Assignee(s)');
- names.push(emptyTooltipLabel);
- }
-
- return names.join(', ');
- },
- sidebarAvatarCounter() {
- let counter = `+${this.users.length - 1}`;
-
- if (this.users.length > this.defaultMaxCounter) {
- counter = `${this.defaultMaxCounter}+`;
- }
+ sortedAssigness() {
+ const canMergeUsers = this.users.filter(user => user.can_merge);
+ const canNotMergeUsers = this.users.filter(user => !user.can_merge);
- return counter;
- },
- mergeNotAllowedTooltipMessage() {
- const assigneesCount = this.users.length;
-
- if (this.issuableType !== 'merge_request' || assigneesCount === 0) {
- return null;
- }
-
- const cannotMergeCount = this.users.filter(u => u.can_merge === false).length;
- const canMergeCount = assigneesCount - cannotMergeCount;
-
- if (canMergeCount === assigneesCount) {
- // Everyone can merge
- return null;
- } else if (cannotMergeCount === assigneesCount && assigneesCount > 1) {
- return __('No one can merge');
- } else if (assigneesCount === 1) {
- return __('Cannot merge');
- }
-
- return sprintf(__('%{canMergeCount}/%{assigneesCount} can merge'), {
- canMergeCount,
- assigneesCount,
- });
+ return [...canMergeUsers, ...canNotMergeUsers];
},
},
methods: {
assignSelf() {
this.$emit('assign-self');
},
- toggleShowLess() {
- this.showLess = !this.showLess;
- },
- renderAssignee(index) {
- return !this.showLess || (index < this.defaultRenderCount && this.showLess);
- },
- avatarUrl(user) {
- return user.avatar || user.avatar_url || gon.default_avatar_url;
- },
- assigneeUrl(user) {
- return `${this.rootPath}${user.username}`;
- },
- assigneeAlt(user) {
- return sprintf(__("%{userName}'s avatar"), { userName: user.name });
- },
- assigneeUsername(user) {
- return `@${user.username}`;
- },
- shouldRenderCollapsedAssignee(index) {
- const firstTwo = this.users.length <= 2 && index <= 2;
-
- return index === 0 || firstTwo;
- },
},
};
</script>
<template>
<div>
- <div
- v-tooltip
- :class="{ 'multiple-users': hasMoreThanOneAssignee }"
- :title="collapsedTooltipTitle"
- class="sidebar-collapsed-icon sidebar-collapsed-user"
- data-container="body"
- data-placement="left"
- data-boundary="viewport"
- >
- <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
- <button
- v-for="(user, index) in users"
- v-if="shouldRenderCollapsedAssignee(index)"
- :key="user.id"
- type="button"
- class="btn-link"
- >
- <img
- :alt="assigneeAlt(user)"
- :src="avatarUrl(user)"
- width="24"
- class="avatar avatar-inline s24"
- />
- <span class="author"> {{ user.name }} </span>
- </button>
- <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
- <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
- </button>
- </div>
+ <collapsed-assignee-list :users="sortedAssigness" :issuable-type="issuableType" />
+
<div class="value hide-collapsed">
- <span
- v-if="mergeNotAllowedTooltipMessage"
- v-tooltip
- :title="mergeNotAllowedTooltipMessage"
- data-placement="left"
- class="float-right cannot-be-merged"
- >
- <i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i>
- </span>
<template v-if="hasNoUsers">
<span class="assign-yourself no-value qa-assign-yourself">
{{ __('None') }}
@@ -200,51 +64,13 @@ export default {
</template>
</span>
</template>
- <template v-else-if="hasOneUser">
- <a :href="assigneeUrl(firstUser)" class="author-link bold">
- <img
- :alt="assigneeAlt(firstUser)"
- :src="avatarUrl(firstUser)"
- width="32"
- class="avatar avatar-inline s32"
- />
- <span class="author"> {{ firstUser.name }} </span>
- <span class="username"> {{ assigneeUsername(firstUser) }} </span>
- </a>
- </template>
- <template v-else>
- <div class="user-list">
- <div
- v-for="(user, index) in users"
- v-if="renderAssignee(index)"
- :key="user.id"
- class="user-item"
- >
- <a
- :href="assigneeUrl(user)"
- :data-title="user.name"
- class="user-link has-tooltip"
- data-container="body"
- data-placement="bottom"
- >
- <img
- :alt="assigneeAlt(user)"
- :src="avatarUrl(user)"
- width="32"
- class="avatar avatar-inline s32"
- />
- </a>
- </div>
- </div>
- <div v-if="renderShowMoreSection" class="user-list-more">
- <button type="button" class="btn-link" @click="toggleShowLess">
- <template v-if="showLess">
- {{ hiddenAssigneesLabel }}
- </template>
- <template v-else>{{ __('- show less') }}</template>
- </button>
- </div>
- </template>
+
+ <uncollapsed-assignee-list
+ v-else
+ :users="sortedAssigness"
+ :root-path="rootPath"
+ :issuable-type="issuableType"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
new file mode 100644
index 00000000000..2f654409561
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue
@@ -0,0 +1,27 @@
+<script>
+import AssigneeAvatar from './assignee_avatar.vue';
+
+export default {
+ components: {
+ AssigneeAvatar,
+ },
+ props: {
+ user: {
+ type: Object,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+};
+</script>
+
+<template>
+ <button type="button" class="btn-link">
+ <assignee-avatar :user="user" :img-size="24" :issuable-type="issuableType" />
+ <span class="author"> {{ user.name }} </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
new file mode 100644
index 00000000000..5b4a43399ca
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -0,0 +1,121 @@
+<script>
+import { __, sprintf } from '~/locale';
+import { GlTooltipDirective } from '@gitlab/ui';
+import CollapsedAssignee from './collapsed_assignee.vue';
+
+const DEFAULT_MAX_COUNTER = 99;
+const DEFAULT_RENDER_COUNT = 5;
+
+export default {
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ CollapsedAssignee,
+ },
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ computed: {
+ isMergeRequest() {
+ return this.issuableType === 'merge_request';
+ },
+ hasNoUsers() {
+ return !this.users.length;
+ },
+ hasMoreThanOneAssignee() {
+ return this.users.length > 1;
+ },
+ hasMoreThanTwoAssignees() {
+ return this.users.length > 2;
+ },
+ allAssigneesCanMerge() {
+ return this.users.every(user => user.can_merge);
+ },
+ sidebarAvatarCounter() {
+ if (this.users.length > DEFAULT_MAX_COUNTER) {
+ return `${DEFAULT_MAX_COUNTER}+`;
+ }
+
+ return `+${this.users.length - 1}`;
+ },
+ collapsedUsers() {
+ const collapsedLength = this.hasMoreThanTwoAssignees ? 1 : this.users.length;
+
+ return this.users.slice(0, collapsedLength);
+ },
+ tooltipTitleMergeStatus() {
+ if (!this.isMergeRequest) {
+ return '';
+ }
+
+ const mergeLength = this.users.filter(u => u.can_merge).length;
+
+ if (mergeLength === this.users.length) {
+ return '';
+ } else if (mergeLength > 0) {
+ return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
+ mergeLength,
+ usersLength: this.users.length,
+ });
+ }
+
+ return this.users.length === 1 ? __('cannot merge') : __('no one can merge');
+ },
+ tooltipTitle() {
+ const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length);
+ const renderUsers = this.users.slice(0, maxRender);
+ const names = renderUsers.map(u => u.name);
+
+ if (!this.users.length) {
+ return __('Assignee(s)');
+ }
+
+ if (this.users.length > names.length) {
+ names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length }));
+ }
+
+ const text = names.join(', ');
+
+ return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text;
+ },
+
+ tooltipOptions() {
+ return { container: 'body', placement: 'left', boundary: 'viewport' };
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-gl-tooltip="tooltipOptions"
+ :class="{ 'multiple-users': hasMoreThanOneAssignee }"
+ :title="tooltipTitle"
+ class="sidebar-collapsed-icon sidebar-collapsed-user"
+ >
+ <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i>
+ <collapsed-assignee
+ v-for="user in collapsedUsers"
+ :key="user.id"
+ :user="user"
+ :issuable-type="issuableType"
+ />
+ <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
+ <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
+ <i
+ v-if="isMergeRequest && !allAssigneesCanMerge"
+ aria-hidden="true"
+ class="fa fa-exclamation-triangle merge-icon"
+ ></i>
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index be1e4811856..c6cc04a139f 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -29,7 +29,7 @@ export default {
},
issuableType: {
type: String,
- require: true,
+ required: false,
default: 'issue',
},
},
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
new file mode 100644
index 00000000000..3a4623121f4
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -0,0 +1,96 @@
+<script>
+import { __, sprintf } from '~/locale';
+import AssigneeAvatarLink from './assignee_avatar_link.vue';
+
+const DEFAULT_RENDER_COUNT = 5;
+
+export default {
+ components: {
+ AssigneeAvatarLink,
+ },
+ props: {
+ users: {
+ type: Array,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: 'issue',
+ },
+ },
+ data() {
+ return {
+ showLess: true,
+ };
+ },
+ computed: {
+ firstUser() {
+ return this.users[0];
+ },
+ hasOneUser() {
+ return this.users.length === 1;
+ },
+ hiddenAssigneesLabel() {
+ const { numberOfHiddenAssignees } = this;
+ return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees });
+ },
+ renderShowMoreSection() {
+ return this.users.length > DEFAULT_RENDER_COUNT;
+ },
+ numberOfHiddenAssignees() {
+ return this.users.length - DEFAULT_RENDER_COUNT;
+ },
+ uncollapsedUsers() {
+ const uncollapsedLength = this.showLess
+ ? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
+ : this.users.length;
+ return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
+ },
+ username() {
+ return `@${this.firstUser.username}`;
+ },
+ },
+ methods: {
+ toggleShowLess() {
+ this.showLess = !this.showLess;
+ },
+ },
+};
+</script>
+
+<template>
+ <assignee-avatar-link
+ v-if="hasOneUser"
+ v-slot="{ user }"
+ tooltip-placement="left"
+ :tooltip-has-name="false"
+ :user="firstUser"
+ :root-path="rootPath"
+ :issuable-type="issuableType"
+ >
+ <div class="ml-2">
+ <span class="author"> {{ user.name }} </span>
+ <span class="username"> {{ username }} </span>
+ </div>
+ </assignee-avatar-link>
+ <div v-else>
+ <div class="user-list">
+ <div v-for="user in uncollapsedUsers" :key="user.id" class="user-item">
+ <assignee-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" />
+ </div>
+ </div>
+ <div v-if="renderShowMoreSection" class="user-list-more">
+ <button type="button" class="btn-link" @click="toggleShowLess">
+ <template v-if="showLess">
+ {{ hiddenAssigneesLabel }}
+ </template>
+ <template v-else>{{ __('- show less') }}</template>
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 597b723a9d9..1c75b6148e8 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -5,6 +5,7 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
components: {
@@ -51,6 +52,11 @@ export default {
toggleForm() {
this.edit = !this.edit;
},
+ onEditClick() {
+ this.toggleForm();
+
+ trackEvent('click_edit_button', 'confidentiality');
+ },
updateConfidentialAttribute(confidential) {
this.service
.update('issue', { confidential })
@@ -82,7 +88,7 @@ export default {
v-if="isEditable"
class="float-right confidential-edit"
href="#"
- @click.prevent="toggleForm"
+ @click.prevent="onEditClick"
>
{{ __('Edit') }}
</a>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index c5cfa92f3c8..ec2a7b93a98 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -6,6 +6,7 @@ import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
components: {
@@ -65,7 +66,11 @@ export default {
toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
+ onEditClick() {
+ this.toggleForm();
+ trackEvent('click_edit_button', 'lock_issue');
+ },
updateLockedAttribute(locked) {
this.mediator.service
.update(this.issuableType, {
@@ -109,7 +114,7 @@ export default {
v-if="isEditable"
class="float-right lock-edit"
type="button"
- @click.prevent="toggleForm"
+ @click.prevent="onEditClick"
>
{{ __('Edit') }}
</button>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
index 0d1faceef11..1f5f19d1931 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
@@ -4,6 +4,7 @@ import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
+import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off';
@@ -63,6 +64,8 @@ export default {
// Component event emission.
this.$emit('toggleSubscription', this.id);
+
+ trackEvent('toggle_button', 'notifications', this.subscribed ? 0 : 1);
},
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 33cedf78331..12c939aa70f 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -62,6 +62,8 @@ function UsersSelect(currentUser, els, options = {}) {
options.showCurrentUser = $dropdown.data('currentUser');
options.todoFilter = $dropdown.data('todoFilter');
options.todoStateFilter = $dropdown.data('todoStateFilter');
+ options.iid = $dropdown.data('iid');
+ options.issuableType = $dropdown.data('issuableType');
showNullUser = $dropdown.data('nullUser');
defaultNullUser = $dropdown.data('nullUserDefault');
showMenuAbove = $dropdown.data('showMenuAbove');
@@ -239,7 +241,7 @@ function UsersSelect(currentUser, els, options = {}) {
'<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>',
);
assigneeTemplate = _.template(
- `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
+ `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), {
openingTag: '<a href="#" class="js-assign-yourself">',
closingTag: '</a>',
@@ -423,6 +425,8 @@ function UsersSelect(currentUser, els, options = {}) {
const { $el, e, isMarking } = options;
const user = options.selectedObj;
+ $el.tooltip('dispose');
+
if ($dropdown.hasClass('js-multiselect')) {
const isActive = $el.hasClass('is-active');
const previouslySelected = $dropdown
@@ -570,20 +574,11 @@ function UsersSelect(currentUser, els, options = {}) {
user.name,
)}</a></li>`;
} else {
- img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
+ // 0 margin, because it's now handled by a wrapper
+ img = "<img src='" + avatar + "' class='avatar avatar-inline m-0' width='32' />";
}
- return `
- <li data-user-id=${user.id}>
- <a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'>
- ${img}
- <strong class='dropdown-menu-user-full-name'>
- ${_.escape(user.name)}
- </strong>
- ${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''}
- </a>
- </li>
- `;
+ return _this.renderRow(options.issuableType, user, selected, username, img);
},
});
};
@@ -764,6 +759,11 @@ UsersSelect.prototype.users = function(query, options, callback) {
author_id: options.authorId || null,
skip_users: options.skipUsers || null,
};
+
+ if (options.issuableType === 'merge_request') {
+ params.merge_request_iid = options.iid || null;
+ }
+
return axios.get(url, { params }).then(({ data }) => {
callback(data);
});
@@ -776,4 +776,44 @@ UsersSelect.prototype.buildUrl = function(url) {
return url;
};
+UsersSelect.prototype.renderRow = function(issuableType, user, selected, username, img) {
+ const tooltip = issuableType === 'merge_request' && !user.can_merge ? __('Cannot merge') : '';
+ const tooltipClass = tooltip ? `has-tooltip` : '';
+ const selectedClass = selected === true ? 'is-active' : '';
+ const linkClasses = `${selectedClass} ${tooltipClass}`;
+ const tooltipAttributes = tooltip
+ ? `data-container="body" data-placement="left" data-title="${tooltip}"`
+ : '';
+
+ return `
+ <li data-user-id=${user.id}>
+ <a href="#" class="dropdown-menu-user-link d-flex align-items-center ${linkClasses}" ${tooltipAttributes}>
+ ${this.renderRowAvatar(issuableType, user, img)}
+ <span class="d-flex flex-column overflow-hidden">
+ <strong class="dropdown-menu-user-full-name">
+ ${_.escape(user.name)}
+ </strong>
+ ${username ? `<span class="dropdown-menu-user-username">${username}</span>` : ''}
+ </span>
+ </a>
+ </li>
+ `;
+};
+
+UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
+ if (user.beforeDivider) {
+ return img;
+ }
+
+ const mergeIcon =
+ issuableType === 'merge_request' && !user.can_merge
+ ? '<i class="fa fa-exclamation-triangle merge-icon"></i>'
+ : '';
+
+ return `<span class="position-relative mr-2">
+ ${img}
+ ${mergeIcon}
+ </span>`;
+};
+
export default UsersSelect;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index c7b064b8506..339e154affc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -50,6 +50,7 @@ export default {
startTag: '<span class="label-branch">',
endTag: '</span>',
},
+ false,
);
},
},
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index fe5289ff371..f49e69c473b 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -146,6 +146,7 @@ export default {
<span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated">
<file-icon
v-if="!showChangedIcon || file.type === 'tree'"
+ class="file-row-icon"
:file-name="file.name"
:loading="file.loading"
:folder="isTree"
@@ -223,13 +224,8 @@ export default {
white-space: nowrap;
}
-.file-row-name svg {
+.file-row-name .file-row-icon {
margin-right: 2px;
vertical-align: middle;
}
-
-.file-row-name .loading-container {
- display: inline-block;
- margin-right: 4px;
-}
</style>
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
index df19906309c..f0aae20477b 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue
@@ -30,9 +30,16 @@ export default {
},
mounted() {
+ if (window.recaptchaDialogCallback) {
+ throw new Error('recaptchaDialogCallback is already defined!');
+ }
window.recaptchaDialogCallback = this.submit.bind(this);
},
+ beforeDestroy() {
+ window.recaptchaDialogCallback = null;
+ },
+
methods: {
appendRecaptchaScript() {
this.removeRecaptchaScript();
diff --git a/app/assets/javascripts/vue_shared/directives/autofocusonshow.js b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js
new file mode 100644
index 00000000000..4659ec20ceb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js
@@ -0,0 +1,39 @@
+/**
+ * Input/Textarea Autofocus Directive for Vue
+ */
+export default {
+ /**
+ * Set focus when element is rendered, but
+ * is not visible, using IntersectionObserver
+ *
+ * @param {Element} el Target element
+ */
+ inserted(el) {
+ if ('IntersectionObserver' in window) {
+ // Element visibility is dynamic, so we attach observer
+ el.visibilityObserver = new IntersectionObserver(entries => {
+ entries.forEach(entry => {
+ // Combining `intersectionRatio > 0` and
+ // element's `offsetParent` presence will
+ // deteremine if element is truely visible
+ if (entry.intersectionRatio > 0 && entry.target.offsetParent) {
+ entry.target.focus();
+ }
+ });
+ });
+
+ // Bind the observer.
+ el.visibilityObserver.observe(el, { root: document.documentElement });
+ }
+ },
+ /**
+ * Detach observer on unbind hook.
+ *
+ * @param {Element} el Target element
+ */
+ unbind(el) {
+ if (el.visibilityObserver) {
+ el.visibilityObserver.disconnect();
+ }
+ },
+};
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
index d287215096e..89029a58d1e 100644
--- a/app/assets/stylesheets/errors.scss
+++ b/app/assets/stylesheets/errors.scss
@@ -96,7 +96,7 @@ a {
}
.error-nav {
- padding: 0;
+ padding: $gl-padding 0 0;
text-align: center;
li {
diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss
index c6060161dec..c036267a7c8 100644
--- a/app/assets/stylesheets/framework/badges.scss
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -1,6 +1,6 @@
.badge.badge-pill {
font-weight: $gl-font-weight-normal;
background-color: $badge-bg;
- color: $gl-text-color-secondary;
+ color: $gray-800;
vertical-align: baseline;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index f384a49e0ae..e9218dcec67 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -438,6 +438,7 @@ img.emoji {
.w-3rem { width: 3rem; }
.h-12em { height: 12em; }
+.h-32-px { height: 32px;}
.mw-460 { max-width: 460px; }
.mw-6em { max-width: 6em; }
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 1bc597bd4ae..ca737c53318 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -131,7 +131,6 @@
> li:not(.d-none) a {
@include media-breakpoint-down(xs) {
margin-left: 0;
- min-width: 100%;
}
}
}
@@ -233,7 +232,6 @@
.impersonation-btn,
.impersonation-btn:hover {
background-color: $white-light;
- margin-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
index ac3214a07d9..bdeac7e97c0 100644
--- a/app/assets/stylesheets/highlight/common.scss
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -16,3 +16,16 @@
color: $dark-diff-match-bg;
background: $dark-diff-match-color;
}
+
+@mixin diff-expansion($background, $border, $link) {
+ background-color: $background;
+
+ td {
+ border-top: 1px solid $border;
+ border-bottom: 1px solid $border;
+ }
+
+ a {
+ color: $link;
+ }
+}
diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss
index 16893dd047e..cbce0ba3f1e 100644
--- a/app/assets/stylesheets/highlight/themes/dark.scss
+++ b/app/assets/stylesheets/highlight/themes/dark.scss
@@ -111,6 +111,10 @@ $dark-il: #de935f;
color: $dark-line-color;
}
+ .line_expansion {
+ @include diff-expansion($dark-main-bg, $dark-border, $dark-na);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss
index 37fe61b925c..1b61ffa37e3 100644
--- a/app/assets/stylesheets/highlight/themes/monokai.scss
+++ b/app/assets/stylesheets/highlight/themes/monokai.scss
@@ -111,6 +111,10 @@ $monokai-gi: #a6e22e;
color: $monokai-text-color;
}
+ .line_expansion {
+ @include diff-expansion($monokai-bg, $monokai-border, $monokai-k);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss
index b4217aac37a..a7ede266fb5 100644
--- a/app/assets/stylesheets/highlight/themes/none.scss
+++ b/app/assets/stylesheets/highlight/themes/none.scss
@@ -34,8 +34,11 @@
color: $gl-text-color;
}
-// Diff line
+ .line_expansion {
+ @include diff-expansion($gray-light, $white-normal, $gl-text-color);
+ }
+ // Diff line
$none-over-bg: #ded7fc;
$none-expanded-border: #e0e0e0;
$none-expanded-bg: #e0e0e0;
diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
index a4e9eda22c9..6569f3abc8b 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
@@ -115,6 +115,10 @@ $solarized-dark-il: #2aa198;
color: $solarized-dark-pre-color;
}
+ .line_expansion {
+ @include diff-expansion($solarized-dark-line-bg, $solarized-dark-border, $solarized-dark-kd);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
index b604d1ccb6c..4e74a9ea50a 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-light.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -122,6 +122,10 @@ $solarized-light-il: #2aa198;
color: $solarized-light-pre-color;
}
+ .line_expansion {
+ @include diff-expansion($solarized-light-line-bg, $solarized-light-border, $solarized-light-kd);
+ }
+
// Diff line
.line_holder {
&.match .line_content,
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index b3974df8639..973f94c63aa 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -101,24 +101,8 @@ pre.code,
color: $white-code-color;
}
-// Expansion line
.line_expansion {
- background-color: $gray-light;
-
- td {
- border-top: 1px solid $border-color;
- border-bottom: 1px solid $border-color;
- text-align: center;
- }
-
- a {
- color: $blue-600;
- }
-
- .unfold-icon {
- display: inline-block;
- padding: 8px 0;
- }
+ @include diff-expansion($gray-light, $border-color, $blue-600);
}
// Diff line
diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss
index 0f4bdb219a3..b88bd78cf3d 100644
--- a/app/assets/stylesheets/pages/container_registry.scss
+++ b/app/assets/stylesheets/pages/container_registry.scss
@@ -3,10 +3,6 @@
*/
.container-message {
- pre {
- white-space: pre-line;
- }
-
span .btn {
margin: 0;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 66b4f3bad2b..0e844b0e4a5 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -126,6 +126,16 @@
}
}
+.assignee {
+ .merge-icon {
+ color: $orange-500;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ text-shadow: -1px -1px 0 $white-light, 1px -1px 0 $white-light, -1px 1px 0 $white-light, 1px 1px 0 $white-light;
+ }
+}
+
.right-sidebar {
position: fixed;
top: $header-height;
@@ -202,7 +212,6 @@
&.assignee {
.author-link {
display: block;
- padding-left: 42px;
position: relative;
&:hover {
@@ -210,12 +219,6 @@
text-decoration: underline;
}
}
-
- .avatar {
- left: 0;
- position: absolute;
- top: 0;
- }
}
}
}
@@ -354,13 +357,6 @@
margin-top: 0;
}
- .assignee .avatar {
- float: left;
- margin-right: 10px;
- margin-bottom: 0;
- margin-left: 0;
- }
-
.assignee .user-list .avatar {
margin: 0;
}
@@ -521,7 +517,12 @@
display: none;
}
+ .merge-icon {
+ font-size: 10px;
+ }
+
.multiple-users {
+ position: relative;
height: 24px;
margin-bottom: 17px;
margin-top: 4px;
diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss
index 85e9f303dde..0fbf7033aa5 100644
--- a/app/assets/stylesheets/pages/reports.scss
+++ b/app/assets/stylesheets/pages/reports.scss
@@ -48,11 +48,6 @@
padding: $gl-padding-top $gl-padding;
border-top: 1px solid $border-color;
}
-
- .report-block-list-icon .loading-container {
- position: relative;
- left: -2px;
- }
}
.report-block-container {
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index f111c7ca8cc..30a567c3bef 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -36,7 +36,7 @@ class AutocompleteController < ApplicationController
end
def award_emojis
- render json: AwardedEmojiFinder.new(current_user).execute
+ render json: AwardEmojis::CollectUserEmojiService.new(current_user).execute
end
def merge_request_target_branches
diff --git a/app/controllers/concerns/invisible_captcha.rb b/app/controllers/concerns/invisible_captcha.rb
index c9f66e5c194..45c0a5c58ef 100644
--- a/app/controllers/concerns/invisible_captcha.rb
+++ b/app/controllers/concerns/invisible_captcha.rb
@@ -41,9 +41,9 @@ module InvisibleCaptcha
request_information = {
message: message,
env: :invisible_captcha_signup_bot_detected,
- ip: request.ip,
+ remote_ip: request.ip,
request_method: request.request_method,
- fullpath: request.fullpath
+ path: request.fullpath
}
Gitlab::AuthLogger.error(request_information)
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 3489ea78b77..8ea77b994de 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -2,8 +2,8 @@
module IssuableCollections
extend ActiveSupport::Concern
- include CookiesHelper
include SortingHelper
+ include SortingPreference
include Gitlab::IssuableMetadata
include Gitlab::Utils::StrongMemoize
@@ -127,47 +127,8 @@ module IssuableCollections
'opened'
end
- def set_sort_order
- set_sort_order_from_user_preference || set_sort_order_from_cookie || default_sort_order
- end
-
- def set_sort_order_from_user_preference
- return unless current_user
- return unless issuable_sorting_field
-
- user_preference = current_user.user_preference
-
- sort_param = params[:sort]
- sort_param ||= user_preference[issuable_sorting_field]
-
- return sort_param if Gitlab::Database.read_only?
-
- if user_preference[issuable_sorting_field] != sort_param
- user_preference.update(issuable_sorting_field => sort_param)
- end
-
- sort_param
- end
-
- # Implement issuable_sorting_field method on controllers
- # to choose which column to store the sorting parameter.
- def issuable_sorting_field
- nil
- end
-
- def set_sort_order_from_cookie
- sort_param = params[:sort] if params[:sort].present?
- # fallback to legacy cookie value for backward compatibility
- sort_param ||= cookies['issuable_sort']
- sort_param ||= cookies[remember_sorting_key]
-
- sort_value = update_cookie_value(sort_param)
- set_secure_cookie(remember_sorting_key, sort_value)
- sort_value
- end
-
- def remember_sorting_key
- @remember_sorting_key ||= "#{collection_type.downcase}_sort"
+ def legacy_sort_cookie_name
+ 'issuable_sort'
end
def default_sort_order
@@ -178,17 +139,6 @@ module IssuableCollections
end
end
- # Update old values to the actual ones.
- def update_cookie_value(value)
- case value
- when 'id_asc' then sort_value_oldest_created
- when 'id_desc' then sort_value_recently_created
- when 'downvotes_asc' then sort_value_popularity
- when 'downvotes_desc' then sort_value_popularity
- else value
- end
- end
-
def finder
@finder ||= issuable_finder_for(finder_type)
end
diff --git a/app/controllers/concerns/issuable_collections_action.rb b/app/controllers/concerns/issuable_collections_action.rb
index 4ad287c4a13..0a6f684a9fc 100644
--- a/app/controllers/concerns/issuable_collections_action.rb
+++ b/app/controllers/concerns/issuable_collections_action.rb
@@ -32,7 +32,7 @@ module IssuableCollectionsAction
private
- def issuable_sorting_field
+ def sorting_field
case action_name
when 'issues'
Issue::SORTING_PREFERENCE_FIELD
diff --git a/app/controllers/concerns/sorting_preference.rb b/app/controllers/concerns/sorting_preference.rb
new file mode 100644
index 00000000000..a51b68147d5
--- /dev/null
+++ b/app/controllers/concerns/sorting_preference.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module SortingPreference
+ include SortingHelper
+ include CookiesHelper
+
+ def set_sort_order
+ set_sort_order_from_user_preference || set_sort_order_from_cookie || params[:sort] || default_sort_order
+ end
+
+ # Implement sorting_field method on controllers
+ # to choose which column to store the sorting parameter.
+ def sorting_field
+ nil
+ end
+
+ # Implement default_sort_order method on controllers
+ # to choose which default sort should be applied if
+ # sort param is not provided.
+ def default_sort_order
+ nil
+ end
+
+ # Implement legacy_sort_cookie_name method on controllers
+ # to set sort from cookie for backwards compatibility.
+ def legacy_sort_cookie_name
+ nil
+ end
+
+ private
+
+ def set_sort_order_from_user_preference
+ return unless current_user
+ return unless sorting_field
+
+ user_preference = current_user.user_preference
+
+ sort_param = params[:sort]
+ sort_param ||= user_preference[sorting_field]
+
+ return sort_param if Gitlab::Database.read_only?
+
+ if user_preference[sorting_field] != sort_param
+ user_preference.update(sorting_field => sort_param)
+ end
+
+ sort_param
+ end
+
+ def set_sort_order_from_cookie
+ return unless legacy_sort_cookie_name
+
+ sort_param = params[:sort] if params[:sort].present?
+ # fallback to legacy cookie value for backward compatibility
+ sort_param ||= cookies[legacy_sort_cookie_name]
+ sort_param ||= cookies[remember_sorting_key]
+
+ sort_value = update_cookie_value(sort_param)
+ set_secure_cookie(remember_sorting_key, sort_value)
+ sort_value
+ end
+
+ # Convert sorting_field to legacy cookie name for backwards compatibility
+ # :merge_requests_sort => 'mergerequest_sort'
+ # :issues_sort => 'issue_sort'
+ def remember_sorting_key
+ @remember_sorting_key ||= sorting_field
+ .to_s
+ .split('_')[0..-2]
+ .map(&:singularize)
+ .join('')
+ .concat('_sort')
+ end
+
+ # Update old values to the actual ones.
+ def update_cookie_value(value)
+ case value
+ when 'id_asc' then sort_value_oldest_created
+ when 'id_desc' then sort_value_recently_created
+ when 'downvotes_asc' then sort_value_popularity
+ when 'downvotes_desc' then sort_value_popularity
+ else value
+ end
+ end
+end
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 97b343f8b1a..24d178781d6 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -7,12 +7,9 @@ module ToggleAwardEmoji
authenticate_user!
name = params.require(:name)
- if awardable.user_can_award?(current_user)
- awardable.toggle_award_emoji(name, current_user)
-
- todoable = to_todoable(awardable)
- TodoService.new.new_award_emoji(todoable, current_user) if todoable
+ service = AwardEmojis::ToggleService.new(awardable, name, current_user).execute
+ if service[:status] == :success
render json: { ok: true }
else
render json: { ok: false }
@@ -21,18 +18,6 @@ module ToggleAwardEmoji
private
- def to_todoable(awardable)
- case awardable
- when Note
- # we don't create todos for personal snippet comments for now
- awardable.for_personal_snippet? ? nil : awardable.noteable
- when MergeRequest, Issue
- awardable
- when Snippet
- nil
- end
- end
-
def awardable
raise NotImplementedError
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index daeb8fda417..71f18694613 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -4,10 +4,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
include ParamsBackwardCompatibility
include RendersMemberAccess
include OnboardingExperimentHelper
+ include SortingHelper
+ include SortingPreference
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
before_action :set_non_archived_param
- before_action :default_sorting
+ before_action :set_sorting
before_action :projects, only: [:index]
skip_cross_project_access_check :index, :starred
@@ -59,11 +61,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
end
- def default_sorting
- params[:sort] ||= 'latest_activity_desc'
- @sort = params[:sort]
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def load_projects(finder_params)
@total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute
@@ -88,4 +85,17 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
+
+ def set_sorting
+ params[:sort] = set_sort_order
+ @sort = params[:sort]
+ end
+
+ def default_sort_order
+ sort_value_latest_activity
+ end
+
+ def sorting_field
+ Project::SORTING_PREFERENCE_FIELD
+ end
end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index ef86d5f981a..271f2b4b57d 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -3,12 +3,13 @@
class Explore::ProjectsController < Explore::ApplicationController
include ParamsBackwardCompatibility
include RendersMemberAccess
+ include SortingHelper
+ include SortingPreference
before_action :set_non_archived_param
+ before_action :set_sorting
def index
- params[:sort] ||= 'latest_activity_desc'
- @sort = params[:sort]
@projects = load_projects
respond_to do |format|
@@ -23,7 +24,6 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending
params[:trending] = true
- @sort = params[:sort]
@projects = load_projects
respond_to do |format|
@@ -67,4 +67,17 @@ class Explore::ProjectsController < Explore::ApplicationController
prepare_projects_for_rendering(projects)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def set_sorting
+ params[:sort] = set_sort_order
+ @sort = params[:sort]
+ end
+
+ def default_sort_order
+ sort_value_latest_activity
+ end
+
+ def sorting_field
+ Project::SORTING_PREFERENCE_FIELD
+ end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index b04ffe80db4..4125f44d00a 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -92,7 +92,7 @@ class Projects::BlobController < Projects::ApplicationController
def diff
apply_diff_view_cookie!
- @form = Blobs::UnfoldPresenter.new(blob, params.to_unsafe_h)
+ @form = Blobs::UnfoldPresenter.new(blob, diff_params)
# keep only json rendering when
# https://gitlab.com/gitlab-org/gitlab-ce/issues/44988 is done
@@ -239,4 +239,8 @@ class Projects::BlobController < Projects::ApplicationController
def tree_path
@path.rpartition('/').first
end
+
+ def diff_params
+ params.permit(:full, :since, :to, :bottom, :unfold, :offset, :indent)
+ end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index bc9166b9df3..b7fd286bfe0 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -190,7 +190,7 @@ class Projects::IssuesController < Projects::ApplicationController
protected
- def issuable_sorting_field
+ def sorting_field
Issue::SORTING_PREFERENCE_FIELD
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index f4d381244d9..f4cc0a5851b 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -219,7 +219,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
alias_method :issuable, :merge_request
alias_method :awardable, :merge_request
- def issuable_sorting_field
+ def sorting_field
MergeRequest::SORTING_PREFERENCE_FIELD
end
diff --git a/app/controllers/projects/starrers_controller.rb b/app/controllers/projects/starrers_controller.rb
index e4093bed0ef..4efe956e973 100644
--- a/app/controllers/projects/starrers_controller.rb
+++ b/app/controllers/projects/starrers_controller.rb
@@ -5,11 +5,11 @@ class Projects::StarrersController < Projects::ApplicationController
def index
@starrers = UsersStarProjectsFinder.new(@project, params, current_user: @current_user).execute
+ @sort = params[:sort].presence || sort_value_name
+ @starrers = @starrers.preload_users.sort_by_attribute(@sort).page(params[:page])
@public_count = @project.starrers.with_public_profile.size
@total_count = @project.starrers.size
@private_count = @total_count - @public_count
- @sort = params[:sort].presence || sort_value_name
- @starrers = @starrers.sort_by_attribute(@sort).page(params[:page])
end
private
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index d1914c35bd3..b187fdb2723 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -16,6 +16,10 @@ class Projects::WikisController < Projects::ApplicationController
redirect_to(project_wiki_path(@project, @page))
end
+ def new
+ redirect_to project_wiki_path(@project, SecureRandom.uuid, random_title: true)
+ end
+
def pages
@wiki_pages = Kaminari.paginate_array(
@project_wiki.list_pages(sort: params[:sort], direction: params[:direction])
@@ -24,17 +28,25 @@ class Projects::WikisController < Projects::ApplicationController
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
end
+ # `#show` handles a number of scenarios:
+ #
+ # - If `id` matches a WikiPage, then show the wiki page.
+ # - If `id` is a file in the wiki repository, then send the file.
+ # - If we know the user wants to create a new page with the given `id`,
+ # then display a create form.
+ # - Otherwise show the empty wiki page and invite the user to create a page.
def show
- view_param = @project_wiki.empty? ? params[:view] : 'create'
-
if @page
set_encoding_error unless valid_encoding?
render 'show'
elsif file_blob
send_blob(@project_wiki.repository, file_blob)
- elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
- @page = build_page(title: params[:id])
+ elsif show_create_form?
+ # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new
+ title = params[:id] unless params[:random_title].present?
+
+ @page = build_page(title: title)
render 'edit'
else
@@ -110,6 +122,15 @@ class Projects::WikisController < Projects::ApplicationController
private
+ def show_create_form?
+ can?(current_user, :create_wiki, @project) &&
+ @page.nil? &&
+ # Always show the create form when the wiki has had at least one page created.
+ # Otherwise, we only show the form when the user has navigated from
+ # the 'empty wiki' page
+ (@project_wiki.exists? || params[:view] == 'create')
+ end
+
def load_project_wiki
@project_wiki = load_wiki
@@ -135,7 +156,7 @@ class Projects::WikisController < Projects::ApplicationController
params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha)
end
- def build_page(args)
+ def build_page(args = {})
WikiPage.new(@project_wiki).tap do |page|
page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases
end
diff --git a/app/finders/award_emojis_finder.rb b/app/finders/award_emojis_finder.rb
new file mode 100644
index 00000000000..7320e035409
--- /dev/null
+++ b/app/finders/award_emojis_finder.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+class AwardEmojisFinder
+ attr_reader :awardable, :params
+
+ def initialize(awardable, params = {})
+ @awardable = awardable
+ @params = params
+
+ validate_params
+ end
+
+ def execute
+ awards = awardable.award_emoji
+ awards = by_name(awards)
+ awards = by_awarded_by(awards)
+ awards
+ end
+
+ private
+
+ def by_name(awards)
+ return awards unless params[:name]
+
+ awards.named(params[:name])
+ end
+
+ def by_awarded_by(awards)
+ return awards unless params[:awarded_by]
+
+ awards.awarded_by(params[:awarded_by])
+ end
+
+ def validate_params
+ return unless params.present?
+
+ validate_name_param
+ validate_awarded_by_param
+ end
+
+ def validate_name_param
+ return unless params[:name]
+
+ raise ArgumentError, 'Invalid name param' unless params[:name].in?(Gitlab::Emoji.emojis_names)
+ end
+
+ def validate_awarded_by_param
+ return unless params[:awarded_by]
+
+ # awarded_by can be a `User`, or an ID
+ unless params[:awarded_by].is_a?(User) || params[:awarded_by].to_s.match(/\A\d+\Z/)
+ raise ArgumentError, 'Invalid awarded_by param'
+ end
+ end
+end
diff --git a/app/finders/awarded_emoji_finder.rb b/app/finders/awarded_emoji_finder.rb
deleted file mode 100644
index f0cc17f3b26..00000000000
--- a/app/finders/awarded_emoji_finder.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# Class for retrieving information about emoji awarded _by_ a particular user.
-class AwardedEmojiFinder
- attr_reader :current_user
-
- # current_user - The User to generate the data for.
- def initialize(current_user = nil)
- @current_user = current_user
- end
-
- def execute
- return [] unless current_user
-
- # We want the resulting data set to be an Array containing the emoji names
- # in descending order, based on how often they were awarded.
- AwardEmoji
- .award_counts_for_user(current_user)
- .map { |name, _| { name: name } }
- end
-end
diff --git a/app/graphql/mutations/award_emojis/add.rb b/app/graphql/mutations/award_emojis/add.rb
index 8e050dd6d29..85f3eb065bb 100644
--- a/app/graphql/mutations/award_emojis/add.rb
+++ b/app/graphql/mutations/award_emojis/add.rb
@@ -10,14 +10,11 @@ module Mutations
check_object_is_awardable!(awardable)
- # TODO this will be handled by AwardEmoji::AddService
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
- award = awardable.create_award_emoji(args[:name], current_user)
+ service = ::AwardEmojis::AddService.new(awardable, args[:name], current_user).execute
{
- award_emoji: (award if award.persisted?),
- errors: errors_on_object(award)
+ award_emoji: (service[:award] if service[:status] == :success),
+ errors: service[:errors] || []
}
end
end
diff --git a/app/graphql/mutations/award_emojis/remove.rb b/app/graphql/mutations/award_emojis/remove.rb
index 3ba85e445b8..f8a3d0ce390 100644
--- a/app/graphql/mutations/award_emojis/remove.rb
+++ b/app/graphql/mutations/award_emojis/remove.rb
@@ -10,22 +10,11 @@ module Mutations
check_object_is_awardable!(awardable)
- # TODO this check can be removed once AwardEmoji services are available.
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
- unless awardable.awarded_emoji?(args[:name], current_user)
- raise Gitlab::Graphql::Errors::ResourceNotAvailable,
- 'You have not awarded emoji of type name to the awardable'
- end
-
- # TODO this will be handled by AwardEmoji::DestroyService
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
- awardable.remove_award_emoji(args[:name], current_user)
+ service = ::AwardEmojis::DestroyService.new(awardable, args[:name], current_user).execute
{
# Mutation response is always a `nil` award_emoji
- errors: []
+ errors: service[:errors] || []
}
end
end
diff --git a/app/graphql/mutations/award_emojis/toggle.rb b/app/graphql/mutations/award_emojis/toggle.rb
index c03902e8035..d822048f3a6 100644
--- a/app/graphql/mutations/award_emojis/toggle.rb
+++ b/app/graphql/mutations/award_emojis/toggle.rb
@@ -15,23 +15,15 @@ module Mutations
check_object_is_awardable!(awardable)
- # TODO this will be handled by AwardEmoji::ToggleService
- # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782
- award = awardable.toggle_award_emoji(args[:name], current_user)
-
- # Destroy returns a collection :(
- award = award.first if award.is_a?(Array)
-
- errors = errors_on_object(award)
+ service = ::AwardEmojis::ToggleService.new(awardable, args[:name], current_user).execute
toggled_on = awardable.awarded_emoji?(args[:name], current_user)
{
# For consistency with the AwardEmojis::Remove mutation, only return
# the AwardEmoji if it was created and not destroyed
- award_emoji: (award if toggled_on),
- errors: errors,
+ award_emoji: (service[:award] if toggled_on),
+ errors: service[:errors] || [],
toggled_on: toggled_on
}
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index e2e007eee50..b88b25eb845 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -405,7 +405,11 @@ module IssuablesHelper
placement: is_collapsed ? 'left' : nil,
container: is_collapsed ? 'body' : nil,
boundary: 'viewport',
- is_collapsed: is_collapsed
+ is_collapsed: is_collapsed,
+ track_label: "right_sidebar",
+ track_property: "update_todo",
+ track_event: "click_button",
+ track_value: ""
}
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5d292094a05..3683f2ea9a9 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -125,9 +125,8 @@ class Notify < BaseMailer
def mail_thread(model, headers = {})
add_project_headers
add_unsubscription_headers_and_links
+ add_model_headers(model)
- headers["X-GitLab-#{model.class.name}-ID"] = model.id
- headers["X-GitLab-#{model.class.name}-IID"] = model.iid if model.respond_to?(:iid)
headers['X-GitLab-Reply-Key'] = reply_key
@reason = headers['X-GitLab-NotificationReason']
@@ -196,6 +195,18 @@ class Notify < BaseMailer
@reply_key ||= SentNotification.reply_key
end
+ # This method applies threading headers to the email to identify
+ # the instance we are discussing.
+ #
+ # All model instances must have `#id`, and may implement `#iid`.
+ def add_model_headers(object)
+ # Use replacement so we don't strip the module.
+ prefix = "X-GitLab-#{object.class.name.gsub(/::/, '-')}"
+
+ headers["#{prefix}-ID"] = object.id
+ headers["#{prefix}-IID"] = object.iid if object.respond_to?(:iid)
+ end
+
def add_project_headers
return unless @project
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index e26162f6151..0ab302a0f3e 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -16,8 +16,10 @@ class AwardEmoji < ApplicationRecord
participant :user
- scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
- scope :upvotes, -> { where(name: UPVOTE_NAME) }
+ scope :downvotes, -> { named(DOWNVOTE_NAME) }
+ scope :upvotes, -> { named(UPVOTE_NAME) }
+ scope :named, -> (names) { where(name: names) }
+ scope :awarded_by, -> (users) { where(user: users) }
after_save :expire_etag_cache
after_destroy :expire_etag_cache
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 14bc56f0eee..f229b42ade6 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -106,30 +106,6 @@ module Awardable
end
def awarded_emoji?(emoji_name, current_user)
- award_emoji.where(name: emoji_name, user: current_user).exists?
- end
-
- def create_award_emoji(name, current_user)
- return unless emoji_awardable?
-
- award_emoji.create(name: normalize_name(name), user: current_user)
- end
-
- def remove_award_emoji(name, current_user)
- award_emoji.where(name: name, user: current_user).destroy_all # rubocop: disable DestroyAll
- end
-
- def toggle_award_emoji(emoji_name, current_user)
- if awarded_emoji?(emoji_name, current_user)
- remove_award_emoji(emoji_name, current_user)
- else
- create_award_emoji(emoji_name, current_user)
- end
- end
-
- private
-
- def normalize_name(name)
- Gitlab::Emoji.normalize_emoji_name(name)
+ award_emoji.named(emoji_name).awarded_by(current_user).exists?
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 8efe4b06f87..10679fb1f85 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -55,6 +55,8 @@ class Project < ApplicationRecord
VALID_MIRROR_PORTS = [22, 80, 443].freeze
VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
+ SORTING_PREFERENCE_FIELD = :projects_sort
+
cache_markdown_field :description, pipeline: :description
delegate :feature_available?, :builds_enabled?, :wiki_enabled?,
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index c91add6439f..4a19e05bf76 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -85,6 +85,10 @@ class ProjectWiki
list_pages(limit: 1).empty?
end
+ def exists?
+ !empty?
+ end
+
# Lists wiki pages of the repository.
#
# limit - max number of pages returned by the method.
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 6f63cd32da4..b957b9b0bdd 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -239,13 +239,13 @@ class Repository
def branch_exists?(branch_name)
return false unless raw_repository
- branch_names.include?(branch_name)
+ branch_names_include?(branch_name)
end
def tag_exists?(tag_name)
return false unless raw_repository
- tag_names.include?(tag_name)
+ tag_names_include?(tag_name)
end
def ref_exists?(ref)
@@ -565,10 +565,10 @@ class Repository
end
delegate :branch_names, to: :raw_repository
- cache_method :branch_names, fallback: []
+ cache_method_as_redis_set :branch_names, fallback: []
delegate :tag_names, to: :raw_repository
- cache_method :tag_names, fallback: []
+ cache_method_as_redis_set :tag_names, fallback: []
delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository
cache_method :branch_count, fallback: 0
@@ -1130,6 +1130,10 @@ class Repository
@cache ||= Gitlab::RepositoryCache.new(self)
end
+ def redis_set_cache
+ @redis_set_cache ||= Gitlab::RepositorySetCache.new(self)
+ end
+
def request_store_cache
@request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore)
end
diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb
index 3c7a805cc5c..c633e2d8b3d 100644
--- a/app/models/users_star_project.rb
+++ b/app/models/users_star_project.rb
@@ -17,6 +17,7 @@ class UsersStarProject < ApplicationRecord
scope :by_project, -> (project) { where(project_id: project.id) }
scope :with_visible_profile, -> (user) { joins(:user).merge(User.with_visible_profile(user)) }
scope :with_public_profile, -> { joins(:user).merge(User.with_public_profile) }
+ scope :preload_users, -> { preload(:user) }
class << self
def sort_by_attribute(method)
diff --git a/app/presenters/blobs/unfold_presenter.rb b/app/presenters/blobs/unfold_presenter.rb
index 4c6dd6895cf..a256dd05a4d 100644
--- a/app/presenters/blobs/unfold_presenter.rb
+++ b/app/presenters/blobs/unfold_presenter.rb
@@ -1,30 +1,34 @@
# frozen_string_literal: true
-require 'gt_one_coercion'
-
module Blobs
class UnfoldPresenter < BlobPresenter
- include Virtus.model
+ include ActiveModel::Attributes
+ include ActiveModel::AttributeAssignment
include Gitlab::Utils::StrongMemoize
- attribute :full, Boolean, default: false
- attribute :since, GtOneCoercion
- attribute :to, Integer
- attribute :bottom, Boolean
- attribute :unfold, Boolean, default: true
- attribute :offset, Integer
- attribute :indent, Integer, default: 0
+ attribute :full, :boolean, default: false
+ attribute :since, :integer, default: 1
+ attribute :to, :integer, default: 1
+ attribute :bottom, :boolean, default: false
+ attribute :unfold, :boolean, default: true
+ attribute :offset, :integer, default: 0
+ attribute :indent, :integer, default: 0
+
+ alias_method :full?, :full
+ alias_method :bottom?, :bottom
+ alias_method :unfold?, :unfold
def initialize(blob, params)
+ super(blob)
+ self.attributes = params
+
# Load all blob data first as we need to ensure they're all loaded first
# so we can accurately show the rest of the diff when unfolding.
load_all_blob_data
- @subject = blob
@all_lines = blob.data.lines
- super(params)
- self.attributes = prepare_attributes
+ handle_full_or_end!
end
# Returns an array of Gitlab::Diff::Line with match line added
@@ -56,21 +60,18 @@ module Blobs
private
- def prepare_attributes
- return attributes unless full? || to == -1
+ def handle_full_or_end!
+ return unless full? || to == -1
- full_opts = {
- since: 1,
+ self.since = 1 if full?
+
+ self.attributes = {
to: all_lines_size,
bottom: false,
unfold: false,
offset: 0,
indent: 0
}
-
- return full_opts if full?
-
- full_opts.merge(attributes.slice(:since))
end
def all_lines_size
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index 6e91317eb20..d6466ad1cdd 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -18,7 +18,7 @@ class DeploymentEntity < Grape::Entity
end
expose :created_at
- expose :finished_at
+ expose :deployed_at
expose :tag
expose :last?
expose :user, using: UserEntity
diff --git a/app/serializers/deployment_serializer.rb b/app/serializers/deployment_serializer.rb
index 3fd3e1b9cc8..b48037dd53f 100644
--- a/app/serializers/deployment_serializer.rb
+++ b/app/serializers/deployment_serializer.rb
@@ -4,7 +4,7 @@ class DeploymentSerializer < BaseSerializer
entity DeploymentEntity
def represent_concise(resource, opts = {})
- opts[:only] = [:iid, :id, :sha, :created_at, :finished_at, :tag, :last?, :id, ref: [:name]]
+ opts[:only] = [:iid, :id, :sha, :created_at, :deployed_at, :tag, :last?, :id, ref: [:name]]
represent(resource, opts)
end
end
diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb
index c02fd024345..058c707ef9d 100644
--- a/app/serializers/issuable_sidebar_basic_entity.rb
+++ b/app/serializers/issuable_sidebar_basic_entity.rb
@@ -4,6 +4,7 @@ class IssuableSidebarBasicEntity < Grape::Entity
include RequestAwareEntity
expose :id
+ expose :iid
expose :type do |issuable|
issuable.to_ability_name
end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index 8ad1df5dfe0..bd2e682a122 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -8,7 +8,7 @@ class MergeRequestSerializer < BaseSerializer
entity ||=
case opts[:serializer]
when 'sidebar'
- IssuableSidebarBasicEntity
+ MergeRequestSidebarBasicEntity
when 'sidebar_extras'
MergeRequestSidebarExtrasEntity
when 'basic'
diff --git a/app/serializers/merge_request_sidebar_basic_entity.rb b/app/serializers/merge_request_sidebar_basic_entity.rb
new file mode 100644
index 00000000000..3c911bbe4c8
--- /dev/null
+++ b/app/serializers/merge_request_sidebar_basic_entity.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity
+ expose :current_user, if: lambda { |_issuable| current_user } do
+ expose :can_merge do |merge_request|
+ merge_request.can_be_merged_by?(current_user)
+ end
+ end
+end
diff --git a/app/services/award_emojis/add_service.rb b/app/services/award_emojis/add_service.rb
new file mode 100644
index 00000000000..eac15dabbf0
--- /dev/null
+++ b/app/services/award_emojis/add_service.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module AwardEmojis
+ class AddService < AwardEmojis::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute
+ unless awardable.user_can_award?(current_user)
+ return error('User cannot award emoji to awardable', status: :forbidden)
+ end
+
+ unless awardable.emoji_awardable?
+ return error('Awardable cannot be awarded emoji', status: :unprocessable_entity)
+ end
+
+ award = awardable.award_emoji.create(name: name, user: current_user)
+
+ if award.persisted?
+ TodoService.new.new_award_emoji(todoable, current_user) if todoable
+ success(award: award)
+ else
+ error(award.errors.full_messages, award: award)
+ end
+ end
+
+ private
+
+ def todoable
+ strong_memoize(:todoable) do
+ case awardable
+ when Note
+ # We don't create todos for personal snippet comments for now
+ awardable.noteable unless awardable.for_personal_snippet?
+ when MergeRequest, Issue
+ awardable
+ when Snippet
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/award_emojis/base_service.rb b/app/services/award_emojis/base_service.rb
new file mode 100644
index 00000000000..a677d03a221
--- /dev/null
+++ b/app/services/award_emojis/base_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module AwardEmojis
+ class BaseService < ::BaseService
+ attr_accessor :awardable, :name
+
+ def initialize(awardable, name, current_user)
+ @awardable = awardable
+ @name = normalize_name(name)
+
+ super(awardable.project, current_user)
+ end
+
+ private
+
+ def normalize_name(name)
+ Gitlab::Emoji.normalize_emoji_name(name)
+ end
+
+ # Provide more error state data than what BaseService allows.
+ # - An array of errors
+ # - The `AwardEmoji` if present
+ def error(errors, award: nil, status: nil)
+ errors = Array.wrap(errors)
+
+ super(errors.to_sentence.presence, status).merge({
+ award: award,
+ errors: errors
+ })
+ end
+ end
+end
diff --git a/app/services/award_emojis/collect_user_emoji_service.rb b/app/services/award_emojis/collect_user_emoji_service.rb
new file mode 100644
index 00000000000..6cab23f3edf
--- /dev/null
+++ b/app/services/award_emojis/collect_user_emoji_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Class for retrieving information about emoji awarded _by_ a particular user.
+module AwardEmojis
+ class CollectUserEmojiService
+ attr_reader :current_user
+
+ # current_user - The User to generate the data for.
+ def initialize(current_user = nil)
+ @current_user = current_user
+ end
+
+ def execute
+ return [] unless current_user
+
+ # We want the resulting data set to be an Array containing the emoji names
+ # in descending order, based on how often they were awarded.
+ AwardEmoji
+ .award_counts_for_user(current_user)
+ .map { |name, _| { name: name } }
+ end
+ end
+end
diff --git a/app/services/award_emojis/destroy_service.rb b/app/services/award_emojis/destroy_service.rb
new file mode 100644
index 00000000000..3789a8403bc
--- /dev/null
+++ b/app/services/award_emojis/destroy_service.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module AwardEmojis
+ class DestroyService < AwardEmojis::BaseService
+ def execute
+ unless awardable.user_can_award?(current_user)
+ return error('User cannot destroy emoji on the awardable', status: :forbidden)
+ end
+
+ awards = AwardEmojisFinder.new(awardable, name: name, awarded_by: current_user).execute
+
+ if awards.empty?
+ return error("User has not awarded emoji of type #{name} on the awardable", status: :forbidden)
+ end
+
+ award = awards.destroy_all.first # rubocop: disable DestroyAll
+
+ success(award: award)
+ end
+ end
+end
diff --git a/app/services/award_emojis/toggle_service.rb b/app/services/award_emojis/toggle_service.rb
new file mode 100644
index 00000000000..812dd1c2889
--- /dev/null
+++ b/app/services/award_emojis/toggle_service.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module AwardEmojis
+ class ToggleService < AwardEmojis::BaseService
+ def execute
+ if awardable.awarded_emoji?(name, current_user)
+ DestroyService.new(awardable, name, current_user).execute
+ else
+ AddService.new(awardable, name, current_user).execute
+ end
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 77c2224ee3b..2ab6e88599f 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -344,10 +344,7 @@ class IssuableBaseService < BaseService
def toggle_award(issuable)
award = params.delete(:emoji_award)
- if award
- todo_service.new_award_emoji(issuable, current_user)
- issuable.toggle_award_emoji(award, current_user)
- end
+ AwardEmojis::ToggleService.new(issuable, award, current_user).execute if award
end
def associations_before_update(issuable)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index e30debbbe75..ee7223d6349 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -598,11 +598,11 @@ module SystemNoteService
end
def zoom_link_added(issue, project, author)
- create_note(NoteSummary.new(issue, project, author, _('a Zoom call was added to this issue'), action: 'pinned_embed'))
+ create_note(NoteSummary.new(issue, project, author, _('added a Zoom call to this issue'), action: 'pinned_embed'))
end
def zoom_link_removed(issue, project, author)
- create_note(NoteSummary.new(issue, project, author, _('a Zoom call was removed from this issue'), action: 'pinned_embed'))
+ create_note(NoteSummary.new(issue, project, author, _('removed a Zoom call from this issue'), action: 'pinned_embed'))
end
private
diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml
index 82781f6716d..86f09bf1cb0 100644
--- a/app/views/admin/applications/_delete_form.html.haml
+++ b/app/views/admin/applications/_delete_form.html.haml
@@ -1,4 +1,4 @@
- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag admin_application_path(application) do
%input{ :name => "_method", :type => "hidden", :value => "delete" }/
- = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css
+ = submit_tag 'Destroy', class: submit_btn_css, data: { confirm: _('Are you sure?') }
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index 69cc510e9c1..9bc5e2ee42f 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -5,4 +5,4 @@
= form_tag path do
%input{ :name => "_method", :type => "hidden", :value => "delete" }/
- = submit_tag _('Revoke'), onclick: "return confirm('#{_('Are you sure?')}')", class: 'btn btn-remove btn-sm'
+ = submit_tag _('Revoke'), class: 'btn btn-remove btn-sm', data: { confirm: _('Are you sure?') }
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index 46931b5932d..1ed7b56db1d 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -10,7 +10,7 @@
= message
%p
= s_('403|Please contact your GitLab administrator to get permission.')
- .action-container.js-go-back{ style: 'display: none' }
- %a{ href: 'javascript:history.back()', class: 'btn btn-success' }
+ .action-container.js-go-back{ hidden: true }
+ %button{ type: 'button', class: 'btn btn-success' }
= s_('Go Back')
= render "errors/footer"
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 74484005b48..dc924a0e25d 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -14,6 +14,10 @@
var goBackElement = document.querySelector('.js-go-back');
if (goBackElement && history.length > 1) {
- goBackElement.style.display = 'block';
+ goBackElement.removeAttribute('hidden');
+
+ goBackElement.querySelector('button').addEventListener('click', function() {
+ history.back();
+ });
}
}());
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 89f99472270..14c7b2428b2 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -4,6 +4,7 @@
- search_path_url = search_path(group_id: group.id)
- else
- search_path_url = search_path
+- has_impersonation_link = header_link?(:admin_impersonation)
%header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } }
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
@@ -64,14 +65,14 @@
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/help_dropdown'
- if header_link?(:user_dropdown)
- %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' } }
+ %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) }
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown'
- - if header_link?(:admin_impersonation)
- %li.nav-item.impersonation
+ - if has_impersonation_link
+ %li.nav-item.impersonation.ml-0
= link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- if header_link?(:sign_in)
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 65ef9690062..08a39fc4f58 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -31,7 +31,7 @@
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.feed-token-reset
= 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()'
+ = text_field_tag :feed_token, current_user.feed_token, class: 'form-control js-select-on-focus', readonly: true
%p.form-text.text-muted
- 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 }
@@ -49,7 +49,7 @@
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.incoming-email-token-reset
= 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()'
+ = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control js-select-on-focus', readonly: true
%p.form-text.text-muted
- 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 }
diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml
index ae9aef5a9b0..e1bf0940f59 100644
--- a/app/views/projects/commit/_ajax_signature.html.haml
+++ b/app/views/projects/commit/_ajax_signature.html.haml
@@ -1,2 +1,2 @@
- if commit.has_signature?
- %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: _('GPG signature (loading...)'), 'commit-sha' => commit.sha } }
+ %button{ tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: _('GPG signature (loading...)'), 'commit-sha' => commit.sha } }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 1331fa179fc..cbd998c60ef 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -24,5 +24,5 @@
= link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
-%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
+%button{ tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 2b56ada8b73..9614f33fe2f 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -43,7 +43,7 @@
} }
Auto DevOps
- if @pipeline.detached_merge_request_pipeline?
- %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: "The code of a detached pipeline is tested against the source branch instead of merged results" }
+ %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results.') }
detached
- if @pipeline.stuck?
%span.js-pipeline-url-stuck.badge.badge-warning
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 858731b2dda..a153f527ee0 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,8 +1,8 @@
-- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
-- commit_message = commit_message % { page_title: @page.title }
+- form_classes = 'wiki-form common-note-form prepend-top-default js-quick-submit'
+- form_classes += ' js-new-wiki-page' unless @page.persisted?
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post,
- html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' },
+ html: { class: form_classes },
data: { uploads_path: uploads_path } do |f|
= form_errors(@page)
@@ -12,12 +12,14 @@
.form-group.row
.col-sm-12= f.label :title, class: 'control-label-full-width'
.col-sm-12
- = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title
- - if @page.persisted?
- %span.d-inline-block.mw-100.prepend-top-5
- = icon('lightbulb-o')
+ = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: _('Wiki|Page title')
+ %span.d-inline-block.mw-100.prepend-top-5
+ = icon('lightbulb-o')
+ - if @page.persisted?
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank'
+ - else
+ = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
.form-group.row
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
@@ -43,7 +45,7 @@
.form-group.row
.col-sm-12= f.label :commit_message, class: 'control-label-full-width'
- .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: commit_message
+ .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: nil
.form-actions
- if @page && @page.persisted?
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 643b51e01d1..2e1e176c42a 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,9 +1,9 @@
- if (@page && @page.persisted?)
- if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do
+ = link_to project_wikis_new_path(@project), class: "add-new-wiki btn btn-success", role: "button" do
= s_("Wiki|New page")
- = link_to project_wiki_history_path(@project, @page), class: "btn" do
+ = link_to project_wiki_history_path(@project, @page), class: "btn", role: "button" do
= s_("Wiki|Page history")
- if can?(current_user, :create_wiki, @project) && @page.latest? && @valid_encoding
- = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit" do
+ = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit", role: "button" do
= _("Edit")
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
deleted file mode 100644
index 2c675c0de9c..00000000000
--- a/app/views/projects/wikis/_new.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-#modal-new-wiki.modal
- .modal-dialog
- .modal-content
- .modal-header
- %h3.page-title= s_("WikiNewPageTitle|New Wiki Page")
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": true } &times;
- .modal-body
- %form.new-wiki-page
- .form-group
- = label_tag :new_wiki_path do
- %span= s_("WikiPage|Page slug")
- = text_field_tag :new_wiki_path, nil, placeholder: s_("WikiNewPagePlaceholder|how-to-setup"), class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true
- %span.d-inline-block.mw-100.prepend-top-5
- = icon('lightbulb-o')
- = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.")
- .form-actions
- = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-success"
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 28353927135..a9d21470944 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -19,5 +19,3 @@
.block
= link_to project_wikis_pages_path(@project), class: 'btn btn-block' do
= s_("Wiki|More Pages")
-
-= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index e8b59a3b8c4..815b4a51261 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -13,14 +13,11 @@
%h2.wiki-page-title
- if @page.persisted?
= link_to @page.human_title, project_wiki_path(@project, @page)
- - else
- = @page.human_title
- %span.light
- &middot;
- - if @page.persisted?
+ %span.light
+ &middot;
= s_("Wiki|Edit Page")
- - else
- = s_("Wiki|Create Page")
+ - else
+ = s_("Wiki|Create New Page")
.nav-controls
- if @page.persisted?
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 825088a58e7..aea09bf8d4d 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -38,7 +38,7 @@
= _('Milestone')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
- if milestone.present?
= link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport' }
@@ -66,7 +66,7 @@
= _('Due date')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" }
.value.hide-collapsed
%span.value-content
- if issuable_sidebar[:due_date]
@@ -102,7 +102,7 @@
= _('Labels')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right'
+ = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right', data: { track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" }
.value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label_hash|
@@ -160,7 +160,7 @@
= custom_icon('icon_arrow_right')
.dropdown.sidebar-move-issue-dropdown.hide-collapsed
%button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
- data: { toggle: 'dropdown', display: 'static' } }
+ data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } }
= _('Move issue')
.dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
= dropdown_title(_('Move issue'))
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index ab01094ed6e..1dc538826dc 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -20,6 +20,8 @@
placeholder: _('Search users'),
data: { first_user: issuable_sidebar.dig(:current_user, :username),
current_user: true,
+ iid: issuable_sidebar[:iid],
+ issuable_type: issuable_type,
project_id: issuable_sidebar[:project_id],
author_id: issuable_sidebar[:author_id],
field_name: "#{issuable_type}[assignee_ids][]",
diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml
index e83ca5eaab8..42a823e3a8d 100644
--- a/app/views/shared/members/_group.html.haml
+++ b/app/views/shared/members/_group.html.haml
@@ -32,7 +32,7 @@
%ul
- Gitlab::Access.options.each do |role, role_id|
%li
- = link_to role, "javascript:void(0)",
+ = link_to role, '#',
class: ("is-active" if group_link.group_access == role_id),
data: { id: role_id, el_id: dom_id }
.clearable-input.member-form-control.d-sm-inline-block
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 331283f7eec..6762f211a80 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -82,7 +82,7 @@
%ul
- member.valid_level_roles.each do |role, role_id|
%li
- = link_to role, "javascript:void(0)",
+ = link_to role, '#',
class: ("is-active" if member.access_level == role_id),
data: { id: role_id, el_id: dom_id(member) }
= render_if_exists 'shared/members/ee/revert_ldap_group_sync_option',
diff --git a/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml b/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml
new file mode 100644
index 00000000000..ffd58067784
--- /dev/null
+++ b/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: Fix HTML rendering for fast-forward rebases in merge request widget
+merge_request: 32032
+author:
+type: fixed
diff --git a/changelogs/unreleased/46299-wiki-page-creation.yml b/changelogs/unreleased/46299-wiki-page-creation.yml
new file mode 100644
index 00000000000..2e8f2accf45
--- /dev/null
+++ b/changelogs/unreleased/46299-wiki-page-creation.yml
@@ -0,0 +1,5 @@
+---
+title: Remove wiki page slug dialog step when creating wiki page
+merge_request: 31362
+author:
+type: changed
diff --git a/changelogs/unreleased/56130-deployed_at.yml b/changelogs/unreleased/56130-deployed_at.yml
new file mode 100644
index 00000000000..c53658de752
--- /dev/null
+++ b/changelogs/unreleased/56130-deployed_at.yml
@@ -0,0 +1,5 @@
+---
+title: Replace finished_at with deployed_at for the internal API Deployment entity
+merge_request: 32000
+author:
+type: other
diff --git a/changelogs/unreleased/59786-show-renamed-file-in-mr.yml b/changelogs/unreleased/59786-show-renamed-file-in-mr.yml
new file mode 100644
index 00000000000..e8c52b592d2
--- /dev/null
+++ b/changelogs/unreleased/59786-show-renamed-file-in-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Fix to show renamed file in mr
+merge_request: 31888
+author:
+type: changed
diff --git a/changelogs/unreleased/62373-badge-counter-very-low-contrast-between-foreground-and-background-c.yml b/changelogs/unreleased/62373-badge-counter-very-low-contrast-between-foreground-and-background-c.yml
new file mode 100644
index 00000000000..12b19da1868
--- /dev/null
+++ b/changelogs/unreleased/62373-badge-counter-very-low-contrast-between-foreground-and-background-c.yml
@@ -0,0 +1,6 @@
+---
+title: 'Resolve Badge counter: Very low contrast between foreground and background
+ colors'
+merge_request: 31922
+author:
+type: other
diff --git a/changelogs/unreleased/64251-branch-name-set-cache.yml b/changelogs/unreleased/64251-branch-name-set-cache.yml
new file mode 100644
index 00000000000..6ce4bdf5e43
--- /dev/null
+++ b/changelogs/unreleased/64251-branch-name-set-cache.yml
@@ -0,0 +1,5 @@
+---
+title: Cache branch and tag names as Redis sets
+merge_request: 30476
+author:
+type: performance
diff --git a/changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml b/changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml
new file mode 100644
index 00000000000..ba4bd614170
--- /dev/null
+++ b/changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 500 errors caused by pattern matching with variables in CI Lint
+merge_request: 31719
+author:
+type: fixed
diff --git a/changelogs/unreleased/65412-add-support-for-line-charts.yml b/changelogs/unreleased/65412-add-support-for-line-charts.yml
new file mode 100644
index 00000000000..cb9043596b7
--- /dev/null
+++ b/changelogs/unreleased/65412-add-support-for-line-charts.yml
@@ -0,0 +1,5 @@
+---
+title: Create component to display area and line charts in monitor dashboards
+merge_request: 31639
+author:
+type: added
diff --git a/changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml b/changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml
new file mode 100644
index 00000000000..d081620c710
--- /dev/null
+++ b/changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml
@@ -0,0 +1,5 @@
+---
+title: Improve system notes for Zoom links
+merge_request: 31410
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/66061-update-tooltip-of-detached-label-state.yml b/changelogs/unreleased/66061-update-tooltip-of-detached-label-state.yml
new file mode 100644
index 00000000000..826d5548999
--- /dev/null
+++ b/changelogs/unreleased/66061-update-tooltip-of-detached-label-state.yml
@@ -0,0 +1,5 @@
+---
+title: Updates tooltip of 'detached' label/state
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml b/changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml
new file mode 100644
index 00000000000..13607ae938a
--- /dev/null
+++ b/changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml
@@ -0,0 +1,5 @@
+---
+title: Add syntax highlighting for line expansion
+merge_request: 31821
+author:
+type: fixed
diff --git a/changelogs/unreleased/66161-replace-expand-icons.yml b/changelogs/unreleased/66161-replace-expand-icons.yml
new file mode 100644
index 00000000000..e28e01108a8
--- /dev/null
+++ b/changelogs/unreleased/66161-replace-expand-icons.yml
@@ -0,0 +1,5 @@
+---
+title: Replaced expand diff icons
+merge_request: 31907
+author:
+type: changed
diff --git a/changelogs/unreleased/an-fix-sidekiq-histogram-buckets.yml b/changelogs/unreleased/an-fix-sidekiq-histogram-buckets.yml
new file mode 100644
index 00000000000..696b8f3987e
--- /dev/null
+++ b/changelogs/unreleased/an-fix-sidekiq-histogram-buckets.yml
@@ -0,0 +1,5 @@
+---
+title: Allow latency measurements of sidekiq jobs taking > 2.5s
+merge_request: 32001
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-mr-commit-note-counter.yml b/changelogs/unreleased/bvl-mr-commit-note-counter.yml
new file mode 100644
index 00000000000..0e9becbd31e
--- /dev/null
+++ b/changelogs/unreleased/bvl-mr-commit-note-counter.yml
@@ -0,0 +1,5 @@
+---
+title: Count comments on commits and merge requests
+merge_request: 31912
+author:
+type: other
diff --git a/changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml b/changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml
new file mode 100644
index 00000000000..e7453a2b9bd
--- /dev/null
+++ b/changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Update assignee (cannot merge) style
+merge_request: 31545
+author:
+type: changed
diff --git a/changelogs/unreleased/ci-config-on-policy.yml b/changelogs/unreleased/ci-config-on-policy.yml
new file mode 100644
index 00000000000..d9804f0323a
--- /dev/null
+++ b/changelogs/unreleased/ci-config-on-policy.yml
@@ -0,0 +1,5 @@
+---
+title: Introduced Build::Rules configuration for Ci::Build
+merge_request: 29011
+author:
+type: added
diff --git a/changelogs/unreleased/clean-obsolete-css.yml b/changelogs/unreleased/clean-obsolete-css.yml
new file mode 100644
index 00000000000..d9716d16762
--- /dev/null
+++ b/changelogs/unreleased/clean-obsolete-css.yml
@@ -0,0 +1,5 @@
+---
+title: Fix MR reports section loading icon alignment
+merge_request: 31897
+author:
+type: fixed
diff --git a/changelogs/unreleased/fe-fix-merge-url-params-with-plus.yml b/changelogs/unreleased/fe-fix-merge-url-params-with-plus.yml
new file mode 100644
index 00000000000..7ca8ae0a8f6
--- /dev/null
+++ b/changelogs/unreleased/fe-fix-merge-url-params-with-plus.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search preserving space when change branch
+merge_request: 31973
+author: minghuan lei
+type: fixed
diff --git a/changelogs/unreleased/fix-file-row-styling.yml b/changelogs/unreleased/fix-file-row-styling.yml
new file mode 100644
index 00000000000..a4427fdd48f
--- /dev/null
+++ b/changelogs/unreleased/fix-file-row-styling.yml
@@ -0,0 +1,5 @@
+---
+title: Fix loading icon causing text to jump in file row of Web IDE
+merge_request: 31884
+author:
+type: fixed
diff --git a/changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml b/changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml
new file mode 100644
index 00000000000..7eed8550acd
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml
@@ -0,0 +1,5 @@
+---
+title: Add persistance to last choice of projects sorting on projects dashboard page
+merge_request: 31669
+author:
+type: added
diff --git a/changelogs/unreleased/pb-update-gitlab-shell-9-4-0.yml b/changelogs/unreleased/pb-update-gitlab-shell-9-4-0.yml
new file mode 100644
index 00000000000..81db7bb4900
--- /dev/null
+++ b/changelogs/unreleased/pb-update-gitlab-shell-9-4-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update to GitLab Shell v9.4.0
+merge_request: 32009
+author:
+type: other
diff --git a/changelogs/unreleased/remove-margin-from-user-header.yml b/changelogs/unreleased/remove-margin-from-user-header.yml
new file mode 100644
index 00000000000..31200d13c93
--- /dev/null
+++ b/changelogs/unreleased/remove-margin-from-user-header.yml
@@ -0,0 +1,5 @@
+---
+title: Remove margin from user header
+merge_request: 30878
+author: lucyfox
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-issues-api-gitaly-nplusone.yml b/changelogs/unreleased/sh-fix-issues-api-gitaly-nplusone.yml
new file mode 100644
index 00000000000..3177cb8d18c
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issues-api-gitaly-nplusone.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Gitaly N+1 calls with listing issues/MRs via API
+merge_request: 31938
+author:
+type: performance
diff --git a/changelogs/unreleased/tr-param-undefined-fix.yml b/changelogs/unreleased/tr-param-undefined-fix.yml
new file mode 100644
index 00000000000..0a9051485bd
--- /dev/null
+++ b/changelogs/unreleased/tr-param-undefined-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fix for embedded metrics undefined params
+merge_request: 31975
+author:
+type: fixed
diff --git a/changelogs/unreleased/update-babel-to-7-5-5.yml b/changelogs/unreleased/update-babel-to-7-5-5.yml
new file mode 100644
index 00000000000..c498e2adfe8
--- /dev/null
+++ b/changelogs/unreleased/update-babel-to-7-5-5.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade babel to 7.5.5
+merge_request: 31819
+author: Takuya Noguchi
+type: other
diff --git a/config/initializers/rack_attack_logging.rb b/config/initializers/rack_attack_logging.rb
index 7eb34bd69e5..b43fff24bb0 100644
--- a/config/initializers/rack_attack_logging.rb
+++ b/config/initializers/rack_attack_logging.rb
@@ -7,9 +7,9 @@ ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, r
rack_attack_info = {
message: 'Rack_Attack',
env: req.env['rack.attack.match_type'],
- ip: req.ip,
+ remote_ip: req.ip,
request_method: req.request_method,
- fullpath: req.fullpath
+ path: req.fullpath
}
if %w(throttle_authenticated_api throttle_authenticated_web).include? req.env['rack.attack.matched']
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 7217f098fd9..9f3e104bc2b 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -28,11 +28,13 @@ if Rails.env.development?
end
enable_json_logs = Gitlab.config.sidekiq.log_format == 'json'
+enable_sidekiq_monitor = ENV.fetch("SIDEKIQ_MONITOR_WORKER", 0).to_i.nonzero?
Sidekiq.configure_server do |config|
config.redis = queues_config_hash
config.server_middleware do |chain|
+ chain.add Gitlab::SidekiqMiddleware::Monitor if enable_sidekiq_monitor
chain.add Gitlab::SidekiqMiddleware::Metrics if Settings.monitoring.sidekiq_exporter
chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] && !enable_json_logs
chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS']
@@ -57,6 +59,8 @@ Sidekiq.configure_server do |config|
# Clear any connections that might have been obtained before starting
# Sidekiq (e.g. in an initializer).
ActiveRecord::Base.clear_all_connections!
+
+ Gitlab::SidekiqMonitor.instance.start if enable_sidekiq_monitor
end
if enable_reliable_fetch?
diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb
index b005fdf159b..af4aec7b355 100644
--- a/config/initializers/zz_metrics.rb
+++ b/config/initializers/zz_metrics.rb
@@ -100,11 +100,7 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Gitlab::Elastic::SnippetSearchResults)
instrumentation.instrument_methods(Gitlab::Elastic::Helper)
- instrumentation.instrument_instance_methods(Elastic::ApplicationSearch)
- instrumentation.instrument_instance_methods(Elastic::IssuesSearch)
- instrumentation.instrument_instance_methods(Elastic::MergeRequestsSearch)
- instrumentation.instrument_instance_methods(Elastic::MilestonesSearch)
- instrumentation.instrument_instance_methods(Elastic::NotesSearch)
+ instrumentation.instrument_instance_methods(Elastic::ApplicationVersionedSearch)
instrumentation.instrument_instance_methods(Elastic::ProjectsSearch)
instrumentation.instrument_instance_methods(Elastic::RepositoriesSearch)
instrumentation.instrument_instance_methods(Elastic::SnippetsSearch)
diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml
index 32475ef8380..08504d6f7d5 100644
--- a/config/prometheus/common_metrics.yml
+++ b/config/prometheus/common_metrics.yml
@@ -166,7 +166,7 @@ panel_groups:
label: Total (cores)
unit: "cores"
- title: "Memory Usage (Pod average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Memory Used per Pod (MB)"
weight: 2
metrics:
@@ -175,7 +175,7 @@ panel_groups:
label: Pod average (MB)
unit: MB
- title: "Canary: Memory Usage (Pod Average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Memory Used per Pod (MB)"
weight: 2
metrics:
@@ -185,7 +185,7 @@ panel_groups:
unit: MB
track: canary
- title: "Core Usage (Pod Average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
@@ -194,7 +194,7 @@ panel_groups:
label: Pod average (cores)
unit: "cores"
- title: "Canary: Core Usage (Pod Average)"
- type: "area-chart"
+ type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
diff --git a/config/routes.rb b/config/routes.rb
index fdef31429f3..d633228a495 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -109,6 +109,7 @@ Rails.application.routes.draw do
Gitlab.ee do
draw :smartcard
draw :jira_connect
+ draw :username
end
Gitlab.ee do
diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb
index 2ca52e55fca..d439c99270e 100644
--- a/config/routes/wiki.rb
+++ b/config/routes/wiki.rb
@@ -2,6 +2,7 @@ scope(controller: :wikis) do
scope(path: 'wikis', as: :wikis) do
get :git_access
get :pages
+ get :new
get '/', to: redirect('%{namespace_id}/%{project_id}/wikis/home')
post '/', to: 'wikis#create'
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index c7586aa1e38..ea165508d29 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -113,3 +113,4 @@
- [elastic_namespace_indexer, 1]
- [export_csv, 1]
- [incident_management, 2]
+ - [jira_connect, 1]
diff --git a/danger/only_documentation/Dangerfile b/danger/only_documentation/Dangerfile
index ff65f8713d2..dad12c0d29c 100644
--- a/danger/only_documentation/Dangerfile
+++ b/danger/only_documentation/Dangerfile
@@ -1,7 +1,7 @@
# rubocop:disable Style/SignalException
# frozen_string_literal: true
-has_only_docs_changes = helper.all_changed_files.all? { |file| file.start_with?('doc/', '.gitlab/ci/docs.gitlab-ci.yml', '.mdlrc') }
+has_only_docs_changes = helper.all_changed_files.all? { |file| file.start_with?('doc/', '.gitlab/ci/docs.gitlab-ci.yml', '.mdlrc') || file.end_with?('.md') }
is_docs_only_branch = gitlab.branch_for_head =~ /(^docs[\/-].*|.*-docs$)/
if is_docs_only_branch && !has_only_docs_changes
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 05bda7d3672..5c8b681fa92 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -1,7 +1,7 @@
require './spec/support/sidekiq'
class Gitlab::Seeder::Pipelines
- STAGES = %w[build test security deploy notify]
+ STAGES = %w[build test deploy notify]
BUILDS = [
# build stage
{ name: 'build:linux', stage: 'build', status: :success,
@@ -31,16 +31,6 @@ class Gitlab::Seeder::Pipelines
{ name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true,
queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
- # security stage
- { name: 'dast', stage: 'security', status: :success,
- queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
- { name: 'sast', stage: 'security', status: :success,
- queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
- { name: 'dependency_scanning', stage: 'security', status: :success,
- queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
- { name: 'container_scanning', stage: 'security', status: :success,
- queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago },
-
# deploy stage
{ name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success,
options: { environment: { action: 'start', on_stop: 'stop staging' } },
@@ -127,11 +117,6 @@ class Gitlab::Seeder::Pipelines
setup_artifacts(build)
setup_test_reports(build)
- if build.ref == build.project.default_branch
- setup_security_reports_file(build)
- else
- setup_security_reports_legacy_archive(build)
- end
setup_build_log(build)
build.project.environments.
@@ -167,55 +152,6 @@ class Gitlab::Seeder::Pipelines
end
end
- def setup_security_reports_file(build)
- return unless build.stage == "security"
-
- # we have two sources: master and feature-branch
- branch_name = build.ref == build.project.default_branch ?
- 'master' : 'feature-branch'
-
- artifacts_cache_file(security_reports_path(branch_name, build.name)) do |file|
- build.job_artifacts.build(
- project: build.project,
- file_type: build.name,
- file_format: :raw,
- file: file)
- end
- end
-
- def setup_security_reports_legacy_archive(build)
- return unless build.stage == "security"
-
- # we have two sources: master and feature-branch
- branch_name = build.ref == build.project.default_branch ?
- 'master' : 'feature-branch'
-
- artifacts_cache_file(security_reports_archive_path(branch_name)) do |file|
- build.job_artifacts.build(
- project: build.project,
- file_type: :archive,
- file_format: :zip,
- file: file)
- end
-
- # assign dummy metadata
- artifacts_cache_file(artifacts_metadata_path) do |file|
- build.job_artifacts.build(
- project: build.project,
- file_type: :metadata,
- file_format: :gzip,
- file: file)
- end
-
- build.options = {
- artifacts: {
- paths: [
- Ci::JobArtifact::DEFAULT_FILE_NAMES.fetch(build.name.to_sym)
- ]
- }
- }
- end
-
def setup_build_log(build)
if %w(running success failed).include?(build.status)
build.trace.set(FFaker::Lorem.paragraphs(6).join("\n\n"))
@@ -267,15 +203,6 @@ class Gitlab::Seeder::Pipelines
Rails.root + 'spec/fixtures/junit/junit.xml.gz'
end
- def security_reports_archive_path(branch)
- Rails.root.join('spec', 'fixtures', 'security-reports', branch + '.zip')
- end
-
- def security_reports_path(branch, name)
- file_name = Ci::JobArtifact::DEFAULT_FILE_NAMES.fetch(name.to_sym)
- Rails.root.join('spec', 'fixtures', 'security-reports', branch, file_name)
- end
-
def artifacts_cache_file(file_path)
file = Tempfile.new("artifacts")
file.close
diff --git a/db/fixtures/development/15_award_emoji.rb b/db/fixtures/development/15_award_emoji.rb
index 137a036edaf..a9dcc048586 100644
--- a/db/fixtures/development/15_award_emoji.rb
+++ b/db/fixtures/development/15_award_emoji.rb
@@ -1,35 +1,22 @@
require './spec/support/sidekiq'
Gitlab::Seeder.quiet do
- emoji = Gitlab::Emoji.emojis.keys
+ EMOJI = Gitlab::Emoji.emojis.keys
- Issue.order(Gitlab::Database.random).limit(Issue.count / 2).each do |issue|
- project = issue.project
+ def seed_award_emoji(klass)
+ klass.order(Gitlab::Database.random).limit(klass.count / 2).each do |awardable|
+ awardable.project.authorized_users.where('project_authorizations.access_level > ?', Gitlab::Access::GUEST).sample(2).each do |user|
+ AwardEmojis::AddService.new(awardable, EMOJI.sample, user).execute
- project.team.users.sample(2).each do |user|
- issue.create_award_emoji(emoji.sample, user)
+ awardable.notes.user.sample(2).each do |note|
+ AwardEmojis::AddService.new(note, EMOJI.sample, user).execute
+ end
- issue.notes.sample(2).each do |note|
- next if note.system?
- note.create_award_emoji(emoji.sample, user)
+ print '.'
end
-
- print '.'
end
end
- MergeRequest.order(Gitlab::Database.random).limit(MergeRequest.count / 2).each do |mr|
- project = mr.project
-
- project.team.users.sample(2).each do |user|
- mr.create_award_emoji(emoji.sample, user)
-
- mr.notes.sample(2).each do |note|
- next if note.system?
- note.create_award_emoji(emoji.sample, user)
- end
-
- print '.'
- end
- end
+ seed_award_emoji(Issue)
+ seed_award_emoji(MergeRequest)
end
diff --git a/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb b/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb
new file mode 100644
index 00000000000..941fead655e
--- /dev/null
+++ b/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddProjectsSortingFieldToUserPreferences < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ add_column :user_preferences, :projects_sort, :string, limit: 64
+ end
+
+ def down
+ remove_column :user_preferences, :projects_sort
+ end
+end
diff --git a/db/migrate/20190814205640_import_common_metrics_line_charts.rb b/db/migrate/20190814205640_import_common_metrics_line_charts.rb
new file mode 100644
index 00000000000..1c28d686a42
--- /dev/null
+++ b/db/migrate/20190814205640_import_common_metrics_line_charts.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ImportCommonMetricsLineCharts < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ce5fd38129a..3f7917654cf 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -3411,6 +3411,7 @@ ActiveRecord::Schema.define(version: 2019_08_15_093949) do
t.string "epics_sort"
t.integer "roadmap_epics_state"
t.string "roadmaps_sort"
+ t.string "projects_sort", limit: 64
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true
end
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index d0adeb89543..d43b3718bf9 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -137,6 +137,15 @@ otherwise you will run into conflicts.
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Validate using:
+
+ ```sh
+ openssl s_client -showcerts -servername gitlab.example.com -connect gitlab.example.com:443 > cacert.pem
+ ```
+
+NOTE: **Note:**
+If your certificate provider provides the CA Bundle certificates, append them to the TLS certificate file.
+
**Installations from source**
1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and
@@ -163,9 +172,13 @@ docker login gitlab.example.com:4567
If the Registry is configured to use its own domain, you will need a TLS
certificate for that specific domain (e.g., `registry.example.com`) or maybe
-a wildcard certificate if hosted under a subdomain of your existing GitLab
+a wildcard certificate if hosted under a subdomain of your existing GitLab
domain (e.g., `registry.gitlab.example.com`).
+NOTE: **Note:**
+As well as manually generated SSL certificates (explained here), certificates automatically
+generated by Let's Encrypt are also [supported in Omnibus installs](https://docs.gitlab.com/omnibus/settings/ssl.html#host-services).
+
Let's assume that you want the container Registry to be accessible at
`https://registry.gitlab.example.com`.
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index d44e141b66b..ba95843b0b0 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -1,4 +1,4 @@
-# Disaster Recovery **(PREMIUM ONLY)**
+# Disaster Recovery (Geo) **(PREMIUM ONLY)**
Geo replicates your database, your Git repositories, and few other assets.
We will support and replicate more data in the future, that will enable you to
diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md
index 5aedb665935..dbd466b906d 100644
--- a/doc/administration/geo/replication/index.md
+++ b/doc/administration/geo/replication/index.md
@@ -1,4 +1,4 @@
-# Geo Replication **(PREMIUM ONLY)**
+# Replication (Geo) **(PREMIUM ONLY)**
> - Introduced in GitLab Enterprise Edition 8.9.
> - Using Geo in combination with
@@ -6,7 +6,7 @@
> is considered **Generally Available** (GA) in
> [GitLab Premium](https://about.gitlab.com/pricing/) 10.4.
-Geo is the solution for widely distributed development teams.
+Replication with Geo is the solution for widely distributed development teams.
## Overview
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 150494c47e5..878f0ef842d 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -64,6 +64,7 @@ The following list depicts what the network architecture of Gitaly is:
topology.
- A `(Gitaly address, Gitaly token)` corresponds to a Gitaly server.
- A Gitaly server hosts one or more storages.
+- A GitLab server can use one or more Gitaly servers.
- Gitaly addresses must be specified in such a way that they resolve
correctly for ALL Gitaly clients.
- Gitaly clients are: Unicorn, Sidekiq, gitlab-workhorse,
@@ -77,14 +78,16 @@ The following list depicts what the network architecture of Gitaly is:
- Authentication is done through a static token which is shared among the Gitaly
and GitLab Rails nodes.
-Below we describe how to configure a Gitaly server at address
-`gitaly.internal:8075` with secret token `abc123secret`. We assume
-your GitLab installation has two repository storages, `default` and
-`storage1`.
+Below we describe how to configure two Gitaly servers one at
+`gitaly1.internal` and the other at `gitaly2.internal`
+with secret token `abc123secret`. We assume
+your GitLab installation has three repository storages: `default`,
+`storage1` and `storage2`.
### 1. Installation
-First install Gitaly using either Omnibus GitLab or install it from source:
+First install Gitaly on each Gitaly server using either
+Omnibus GitLab or install it from source:
- For Omnibus GitLab: [Download/install](https://about.gitlab.com/install/) the Omnibus GitLab
package you want using **steps 1 and 2** from the GitLab downloads page but
@@ -119,7 +122,7 @@ Configure a token on the instance that runs the GitLab Rails application.
### 3. Gitaly server configuration
-Next, on the Gitaly server, you need to configure storage paths, enable
+Next, on the Gitaly servers, you need to configure storage paths, enable
the network listener and configure the token.
NOTE: **Note:** if you want to reduce the risk of downtime when you enable
@@ -175,15 +178,29 @@ Check the directory layout on your Gitaly server to be sure.
gitaly['listen_addr'] = "0.0.0.0:8075"
gitaly['auth_token'] = 'abc123secret'
+ # To use TLS for Gitaly you need to add
+ gitaly['tls_listen_addr'] = "0.0.0.0:9999"
+ gitaly['certificate_path'] = "path/to/cert.pem"
+ gitaly['key_path'] = "path/to/key.pem"
+ ```
+
+1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server:
+
+ For `gitaly1.internal`:
+
+ ```
gitaly['storage'] = [
{ 'name' => 'default' },
{ 'name' => 'storage1' },
]
+ ```
+
+ For `gitaly2.internal`:
- # To use TLS for Gitaly you need to add
- gitaly['tls_listen_addr'] = "0.0.0.0:9999"
- gitaly['certificate_path'] = "path/to/cert.pem"
- gitaly['key_path'] = "path/to/key.pem"
+ ```
+ gitaly['storage'] = [
+ { 'name' => 'storage2' },
+ ]
```
NOTE: **Note:**
@@ -206,13 +223,26 @@ Check the directory layout on your Gitaly server to be sure.
[auth]
token = 'abc123secret'
+ ```
+
+1. Append the following to `/home/git/gitaly/config.toml` for each respective server:
+
+ For `gitaly1.internal`:
+ ```toml
[[storage]]
name = 'default'
[[storage]]
name = 'storage1'
```
+
+ For `gitaly2.internal`:
+
+ ```toml
+ [[storage]]
+ name = 'storage2'
+ ```
NOTE: **Note:**
In some cases, you'll have to set `path` for each `[[storage]]` in the
@@ -231,9 +261,13 @@ then all Gitaly requests will fail.
Additionally, you need to
[disable Rugged if previously manually enabled](../high_availability/nfs.md#improving-nfs-performance-with-gitlab).
-We assume that your Gitaly server can be reached at
-`gitaly.internal:8075` from your GitLab server, and that Gitaly can read and
-write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1` respectively.
+We assume that your `gitaly1.internal` Gitaly server can be reached at
+`gitaly1.internal:8075` from your GitLab server, and that Gitaly server
+can read and write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1`.
+
+We assume also that your `gitaly2.internal` Gitaly server can be reached at
+`gitaly2.internal:8075` from your GitLab server, and that Gitaly server
+can read and write to `/mnt/gitlab/storage2`.
**For Omnibus GitLab**
@@ -241,8 +275,9 @@ write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1` respectively.
```ruby
git_data_dirs({
- 'default' => { 'gitaly_address' => 'tcp://gitaly.internal:8075' },
- 'storage1' => { 'gitaly_address' => 'tcp://gitaly.internal:8075' },
+ 'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
+ 'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
+ 'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' },
})
gitlab_rails['gitaly_token'] = 'abc123secret'
@@ -268,9 +303,11 @@ write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1` respectively.
repositories:
storages:
default:
- gitaly_address: tcp://gitaly.internal:8075
+ gitaly_address: tcp://gitaly1.internal:8075
storage1:
- gitaly_address: tcp://gitaly.internal:8075
+ gitaly_address: tcp://gitaly1.internal:8075
+ storage2:
+ gitaly_address: tcp://gitaly2.internal:8075
gitaly:
token: 'abc123secret'
@@ -350,8 +387,9 @@ To configure Gitaly with TLS:
```ruby
git_data_dirs({
- 'default' => { 'gitaly_address' => 'tls://gitaly.internal:9999' },
- 'storage1' => { 'gitaly_address' => 'tls://gitaly.internal:9999' },
+ 'default' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' },
+ 'storage1' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' },
+ 'storage2' => { 'gitaly_address' => 'tls://gitaly2.internal:9999' },
})
gitlab_rails['gitaly_token'] = 'abc123secret'
@@ -377,9 +415,11 @@ To configure Gitaly with TLS:
repositories:
storages:
default:
- gitaly_address: tls://gitaly.internal:9999
+ gitaly_address: tls://gitaly1.internal:9999
storage1:
- gitaly_address: tls://gitaly.internal:9999
+ gitaly_address: tls://gitaly1.internal:9999
+ storage2:
+ gitaly_address: tls://gitaly2.internal:9999
gitaly:
token: 'abc123secret'
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index 9e9f604317a..f11d27487d1 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -60,11 +60,21 @@ for details on managing SSL certificates and configuring Nginx.
### Basic ports
-| LB Port | Backend Port | Protocol |
-| ------- | ------------ | --------------- |
-| 80 | 80 | HTTP [^1] |
-| 443 | 443 | TCP or HTTPS [^1] [^2] |
-| 22 | 22 | TCP |
+| LB Port | Backend Port | Protocol |
+| ------- | ------------ | ------------------------ |
+| 80 | 80 | HTTP (*1*) |
+| 443 | 443 | TCP or HTTPS (*1*) (*2*) |
+| 22 | 22 | TCP |
+
+- (*1*): [Web terminal](../../ci/environments.md#web-terminals) support requires
+ your load balancer to correctly handle WebSocket connections. When using
+ HTTP or HTTPS proxying, this means your load balancer must be configured
+ to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the
+ [web terminal](../integration/terminal.md) integration guide for
+ more details.
+- (*2*): When using HTTPS protocol for port 443, you will need to add an SSL
+ certificate to the load balancers. If you wish to terminate SSL at the
+ GitLab application server instead, use TCP protocol.
### GitLab Pages Ports
@@ -72,12 +82,19 @@ If you're using GitLab Pages with custom domain support you will need some
additional port configurations.
GitLab Pages requires a separate virtual IP address. Configure DNS to point the
`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the
-[GitLab Pages documentation][gitlab-pages] for more information.
+[GitLab Pages documentation](../pages/index.md) for more information.
-| LB Port | Backend Port | Protocol |
-| ------- | ------------ | -------- |
-| 80 | Varies [^3] | HTTP |
-| 443 | Varies [^3] | TCP [^4] |
+| LB Port | Backend Port | Protocol |
+| ------- | ------------- | --------- |
+| 80 | Varies (*1*) | HTTP |
+| 443 | Varies (*1*) | TCP (*2*) |
+
+- (*1*): The backend port for GitLab Pages depends on the
+ `gitlab_pages['external_http']` and `gitlab_pages['external_https']`
+ setting. See [GitLab Pages documentation](../pages/index.md) for more details.
+- (*2*): Port 443 for GitLab Pages should always use the TCP protocol. Users can
+ configure custom domains with custom SSL, which would not be possible
+ if SSL was terminated at the load balancer.
### Alternate SSH Port
@@ -86,7 +103,7 @@ it may be helpful to configure an alternate SSH hostname that allows users
to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address
compared to the other GitLab HTTP configuration above.
-Configure DNS for an alternate SSH hostname such as altssh.gitlab.example.com.
+Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`.
| LB Port | Backend Port | Protocol |
| ------- | ------------ | -------- |
@@ -101,24 +118,6 @@ Read more on high-availability configuration:
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
-[^1]: [Web terminal](../../ci/environments.md#web-terminals) support requires
- your load balancer to correctly handle WebSocket connections. When using
- HTTP or HTTPS proxying, this means your load balancer must be configured
- to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the
- [web terminal](../integration/terminal.md) integration guide for
- more details.
-[^2]: When using HTTPS protocol for port 443, you will need to add an SSL
- certificate to the load balancers. If you wish to terminate SSL at the
- GitLab application server instead, use TCP protocol.
-[^3]: The backend port for GitLab Pages depends on the
- `gitlab_pages['external_http']` and `gitlab_pages['external_https']`
- setting. See [GitLab Pages documentation][gitlab-pages] for more details.
-[^4]: Port 443 for GitLab Pages should always use the TCP protocol. Users can
- configure custom domains with custom SSL, which would not be possible
- if SSL was terminated at the load balancer.
-
-[gitlab-pages]: ../pages/index.md
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index a57ef8ddc7d..9030c941cad 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -88,7 +88,7 @@ Introduced in GitLab 10.0, this file lives in
It helps you see requests made directly to the API. For example:
```json
-{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30,"gitaly_duration":5.36}
+{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","remote_ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30,"gitaly_duration":5.36}
```
This entry above shows an access to an internal endpoint to check whether an
diff --git a/doc/administration/monitoring/gitlab_instance_administration_project/index.md b/doc/administration/monitoring/gitlab_instance_administration_project/index.md
index 8e33cea6217..d445b68721d 100644
--- a/doc/administration/monitoring/gitlab_instance_administration_project/index.md
+++ b/doc/administration/monitoring/gitlab_instance_administration_project/index.md
@@ -27,7 +27,7 @@ If that's not the case or if you have an external Prometheus instance or an HA s
you should
[configure it manually](../../../user/project/integrations/prometheus.md#manual-configuration-of-prometheus).
-## Taking action on Prometheus alerts **[ULTIMATE]**
+## Taking action on Prometheus alerts **(ULTIMATE)**
You can [add a webhook](../../../user/project/integrations/prometheus.md#external-prometheus-instances)
to the Prometheus config in order for GitLab to receive notifications of any alerts.
diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md
index 9f671e0db11..c32edb60f9d 100644
--- a/doc/administration/monitoring/performance/request_profiling.md
+++ b/doc/administration/monitoring/performance/request_profiling.md
@@ -5,7 +5,7 @@
1. Grab the profiling token from **Monitoring > Requests Profiles** admin page
(highlighted in a blue in the image below).
![Profile token](img/request_profiling_token.png)
-1. Pass the header `X-Profile-Token: <token>` and `X-Profile-Mode: <mode>`(where <mode> can be `execution` or `memory`) to the request you want to profile. You can use:
+1. Pass the header `X-Profile-Token: <token>` and `X-Profile-Mode: <mode>`(where `<mode>` can be `execution` or `memory`) to the request you want to profile. You can use:
- Browser extensions. For example, [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension.
- `curl`. For example, `curl --header 'X-Profile-Token: <token>' --header 'X-Profile-Mode: <mode>' https://gitlab.example.com/group/project`.
1. Once request is finished (which will take a little longer than usual), you can
diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
index 95cdb1508fa..238c522a0ee 100644
--- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
+++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
@@ -29,7 +29,7 @@ and they will assist you with any issues you are having.
```bash
# for minikube:
minikube dashboard —url
- # for non-local installations if access via kubectl is configured:
+ # for non-local installations if access via Kubectl is configured:
kubectl proxy
```
@@ -49,7 +49,7 @@ and they will assist you with any issues you are having.
- What to do with pods in `CrashLoopBackoff` status:
- Check logs via Kubernetes dashboard.
- - Check logs via `kubectl`:
+ - Check logs via Kubectl:
```bash
kubectl logs <unicorn pod> -c dependencies
@@ -72,7 +72,7 @@ and they will assist you with any issues you are having.
This is the principle of Kubernetes, read [Twelve-factor app](https://12factor.net/)
for details.
-## Gitlab-specific kubernetes information
+## GitLab-specific kubernetes information
- Minimal config that can be used to test a Kubernetes helm chart can be found
[here](https://gitlab.com/charts/gitlab/issues/620).
@@ -83,7 +83,7 @@ and they will assist you with any issues you are having.
kubectl logs gitlab-unicorn-7656fdd6bf-jqzfs -c unicorn
```
-- It is not possible to get all the logs via `kubectl` at once, like with `gitlab-ctl tail`,
+- It is not possible to get all the logs via Kubectl at once, like with `gitlab-ctl tail`,
but a number of third-party tools can be used to do it:
- [Kubetail](https://github.com/johanhaleby/kubetail)
@@ -131,7 +131,7 @@ and they will assist you with any issues you are having.
- Check the output of `kubectl get events -w --all-namespaces`.
- Check the logs of pods within `gitlab-managed-apps` namespace.
- On the side of GitLab check sidekiq log and kubernetes log. When GitLab is installed
- via helm chart, kubernetes.log can be found inside the sidekiq pod.
+ via Helm Chart, `kubernetes.log` can be found inside the sidekiq pod.
- How to get your initial admin password <https://docs.gitlab.com/charts/installation/deployment.html#initial-login>:
@@ -142,19 +142,19 @@ and they will assist you with any issues you are having.
kubectl get secret <secret-name> -ojsonpath={.data.password} | base64 --decode ; echo
```
-- How to connect to a GitLab postgres database:
+- How to connect to a GitLab Postgres database:
```bash
kubectl exec -it <task-runner-pod-name> -- /srv/gitlab/bin/rails dbconsole -p
```
-- How to get info about helm installation status:
+- How to get info about Helm installation status:
```bash
helm status name-of-installation
```
-- How to update GitLab installed using helm chart:
+- How to update GitLab installed using Helm Chart:
```bash
helm repo upgrade
@@ -179,25 +179,25 @@ and they will assist you with any issues you are having.
helm upgrade <release name> <chart path> -f gitlab.yaml
```
-## Installation of minimal GitLab config via minukube on macOS
+## Installation of minimal GitLab config via Minukube on macOS
This section is based on [Developing for Kubernetes with Minikube](https://gitlab.com/charts/gitlab/blob/master/doc/minikube/index.md)
and [Helm](https://gitlab.com/charts/gitlab/blob/master/doc/helm/index.md). Refer
to those documents for details.
-- Install kubectl via Homebrew:
+- Install Kubectl via Homebrew:
```bash
brew install kubernetes-cli
```
-- Install minikube via Homebrew:
+- Install Minikube via Homebrew:
```bash
brew cask install minikube
```
-- Start minikube and configure it. If minikube cannot start, try running `minikube delete && minikube start`
+- Start Minikube and configure it. If Minikube cannot start, try running `minikube delete && minikube start`
and repeat the steps:
```bash
@@ -206,7 +206,7 @@ to those documents for details.
minikube addons enable kube-dns
```
-- Install helm via Homebrew and initialize it:
+- Install Helm via Homebrew and initialize it:
```bash
brew install kubernetes-helm
@@ -219,7 +219,7 @@ to those documents for details.
- Find the IP address in the output of `minikube ip` and update the yaml file with
this IP address.
-- Install the GitLab helm chart:
+- Install the GitLab Helm Chart:
```bash
helm repo add gitlab https://charts.gitlab.io
diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md
index 7067958ecb4..9b016c64e29 100644
--- a/doc/administration/troubleshooting/sidekiq.md
+++ b/doc/administration/troubleshooting/sidekiq.md
@@ -169,3 +169,121 @@ The PostgreSQL wiki has details on the query you can run to see blocking
queries. The query is different based on PostgreSQL version. See
[Lock Monitoring](https://wiki.postgresql.org/wiki/Lock_Monitoring) for
the query details.
+
+## Managing Sidekiq queues
+
+It is possible to use [Sidekiq API](https://github.com/mperham/sidekiq/wiki/API)
+to perform a number of troubleshoting on Sidekiq.
+
+These are the administrative commands and it should only be used if currently
+admin interface is not suitable due to scale of installation.
+
+All this commands should be run using `gitlab-rails console`.
+
+### View the queue size
+
+```ruby
+Sidekiq::Queue.new("pipeline_processing:build_queue").size
+```
+
+### Enumerate all enqueued jobs
+
+```ruby
+queue = Sidekiq::Queue.new("chaos:chaos_sleep")
+queue.each do |job|
+ # job.klass # => 'MyWorker'
+ # job.args # => [1, 2, 3]
+ # job.jid # => jid
+ # job.queue # => chaos:chaos_sleep
+ # job["retry"] # => 3
+ # job.item # => {
+ # "class"=>"Chaos::SleepWorker",
+ # "args"=>[1000],
+ # "retry"=>3,
+ # "queue"=>"chaos:chaos_sleep",
+ # "backtrace"=>true,
+ # "queue_namespace"=>"chaos",
+ # "jid"=>"39bc482b823cceaf07213523",
+ # "created_at"=>1566317076.266069,
+ # "correlation_id"=>"c323b832-a857-4858-b695-672de6f0e1af",
+ # "enqueued_at"=>1566317076.26761},
+ # }
+
+ # job.delete if job.jid == 'abcdef1234567890'
+end
+```
+
+### Enumerate currently running jobs
+
+```ruby
+workers = Sidekiq::Workers.new
+workers.each do |process_id, thread_id, work|
+ # process_id is a unique identifier per Sidekiq process
+ # thread_id is a unique identifier per thread
+ # work is a Hash which looks like:
+ # {"queue"=>"chaos:chaos_sleep",
+ # "payload"=>
+ # { "class"=>"Chaos::SleepWorker",
+ # "args"=>[1000],
+ # "retry"=>3,
+ # "queue"=>"chaos:chaos_sleep",
+ # "backtrace"=>true,
+ # "queue_namespace"=>"chaos",
+ # "jid"=>"b2a31e3eac7b1a99ff235869",
+ # "created_at"=>1566316974.9215662,
+ # "correlation_id"=>"e484fb26-7576-45f9-bf21-b99389e1c53c",
+ # "enqueued_at"=>1566316974.9229589},
+ # "run_at"=>1566316974}],
+end
+```
+
+### Remove sidekiq jobs for given parameters (destructive)
+
+```ruby
+# for jobs like this:
+# RepositoryImportWorker.new.perform_async(100)
+id_list = [100]
+
+queue = Sidekiq::Queue.new('repository_import')
+queue.each do |job|
+ job.delete if id_list.include?(job.args[0])
+end
+```
+
+### Remove specific job ID (destructive)
+
+```ruby
+queue = Sidekiq::Queue.new('repository_import')
+queue.each do |job|
+ job.delete if job.jid == 'my-job-id'
+end
+```
+
+## Canceling running jobs (destructive)
+
+> Introduced in GitLab 12.3.
+
+This is highly risky operation and use it as last resort.
+Doing that might result in data corruption, as the job
+is interrupted mid-execution and it is not guaranteed
+that proper rollback of transactions is implemented.
+
+```ruby
+Gitlab::SidekiqMonitor.cancel_job('job-id')
+```
+
+> This requires the Sidekiq to be run with `SIDEKIQ_MONITOR_WORKER=1`
+> environment variable.
+
+To perform of the interrupt we use `Thread.raise` which
+has number of drawbacks, as mentioned in [Why Ruby’s Timeout is dangerous (and Thread.raise is terrifying)](https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/):
+
+> This is where the implications get interesting, and terrifying. This means that an exception can get raised:
+>
+> * during a network request (ok, as long as the surrounding code is prepared to catch Timeout::Error)
+> * during the cleanup for the network request
+> * during a rescue block
+> * while creating an object to save to the database afterwards
+> * in any of your code, regardless of whether it could have possibly raised an exception before
+>
+> Nobody writes code to defend against an exception being raised on literally any line. That’s not even possible. So Thread.raise is basically like a sneak attack on your code that could result in almost anything. It would probably be okay if it were pure-functional code that did not modify any state. But this is Ruby, so that’s unlikely :)
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index b32f11464ef..9af5430f1c8 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -67,7 +67,7 @@ The following API resources are available in the project context:
| [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) |
| [Services](services.md) | `/projects/:id/services` |
| [Tags](tags.md) | `/projects/:id/repository/tags` |
-| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` (also available for groups) |
+| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities`
| [Wikis](wikis.md) | `/projects/:id/wikis` |
## Group resources
diff --git a/doc/api/boards.md b/doc/api/boards.md
index 08ec1d832df..b848d7788cd 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -466,7 +466,7 @@ Example response:
## Delete a board list
-Only for admins and project owners. Soft deletes the board list in question.
+Only for admins and project owners. Deletes the board list in question.
```
DELETE /projects/:id/boards/:board_id/lists/:list_id
diff --git a/doc/api/group_boards.md b/doc/api/group_boards.md
index 4d10f83720b..99b522a7ae9 100644
--- a/doc/api/group_boards.md
+++ b/doc/api/group_boards.md
@@ -536,7 +536,7 @@ Example response:
## Delete a group issue board list
-Only for admins and group owners. Soft deletes the board list in question.
+Only for admins and group owners. Deletes the board list in question.
```
DELETE /groups/:id/boards/:board_id/lists/:list_id
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 96a547551f1..4f2b4a966c9 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -136,7 +136,6 @@ Example response:
"award_emoji":"http://example.com/api/v4/projects/1/issues/76/award_emoji",
"project":"http://example.com/api/v4/projects/1"
},
- "subscribed": false,
"task_completion_status":{
"count":0,
"completed_count":0
@@ -441,7 +440,6 @@ Example response:
"award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji",
"project":"http://example.com/api/v4/projects/4"
},
- "subscribed": false,
"task_completion_status":{
"count":0,
"completed_count":0
@@ -790,7 +788,7 @@ the `weight` parameter:
## Delete an issue
-Only for admins and project owners. Soft deletes the issue in question.
+Only for admins and project owners. Deletes the issue in question.
```
DELETE /projects/:id/issues/:issue_iid
diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md
index cc95689a65f..bf83bfd7e6a 100644
--- a/doc/api/merge_request_approvals.md
+++ b/doc/api/merge_request_approvals.md
@@ -23,36 +23,6 @@ GET /projects/:id/approvals
```json
{
- "approvers": [
- {
- "user": {
- "id": 5,
- "name": "John Doe6",
- "username": "user5",
- "state":"active","avatar_url":"https://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80\u0026d=identicon","web_url":"http://localhost/user5"
- }
- }
- ],
- "approver_groups": [
- {
- "group": {
- "id": 1,
- "name": "group1",
- "path": "group1",
- "description": "",
- "visibility": "public",
- "lfs_enabled": false,
- "avatar_url": null,
- "web_url": "http://localhost/groups/group1",
- "request_access_enabled": false,
- "full_name": "group1",
- "full_path": "group1",
- "parent_id": null,
- "ldap_cn": null,
- "ldap_access": null
- }
- }
- ],
"approvals_before_merge": 2,
"reset_approvals_on_push": true,
"disable_overriding_approvers_per_merge_request": false
@@ -75,7 +45,7 @@ POST /projects/:id/approvals
| Attribute | Type | Required | Description |
| ------------------------------------------------ | ------- | -------- | --------------------------------------------------------------------------------------------------- |
| `id` | integer | yes | The ID of a project |
-| `approvals_before_merge` | integer | no | How many approvals are required before an MR can be merged |
+| `approvals_before_merge` | integer | no | How many approvals are required before an MR can be merged. Deprecated in 12.0 in favor of Approval Rules API. |
| `reset_approvals_on_push` | boolean | no | Reset approvals on a new push |
| `disable_overriding_approvers_per_merge_request` | boolean | no | Allow/Disallow overriding approvers per MR |
| `merge_requests_author_approval` | boolean | no | Allow/Disallow authors from self approving merge requests; `true` means authors cannot self approve |
@@ -83,20 +53,68 @@ POST /projects/:id/approvals
```json
{
- "approvers": [
- {
- "user": {
+ "approvals_before_merge": 2,
+ "reset_approvals_on_push": true,
+ "disable_overriding_approvers_per_merge_request": false,
+ "merge_requests_author_approval": false,
+ "merge_requests_disable_committers_approval": false
+}
+```
+
+### Get project-level rules
+
+>**Note:** This API endpoint is only available on 12.3 Starter and above.
+
+You can request information about a project's approval rules using the following endpoint:
+
+```
+GET /projects/:id/approval_rules
+```
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|----------------------|---------|----------|-----------------------------------------------------------|
+| `id` | integer | yes | The ID of a project |
+
+```json
+[
+ {
+ "id": 1,
+ "name": "security",
+ "rule_type": "regular",
+ "eligible_approvers": [
+ {
"id": 5,
- "name": "John Doe6",
- "username": "user5",
- "state":"active","avatar_url":"https://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80\u0026d=identicon","web_url":"http://localhost/user5"
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ },
+ {
+ "id": 50,
+ "name": "Group Member 1",
+ "username": "group_member_1",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/group_member_1"
}
- }
- ],
- "approver_groups": [
- {
- "group": {
- "id": 1,
+ ],
+ "approvals_required": 3,
+ "users": [
+ {
+ "id": 5,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ }
+ ],
+ "groups": [
+ {
+ "id": 5,
"name": "group1",
"path": "group1",
"description": "",
@@ -111,18 +129,187 @@ POST /projects/:id/approvals
"ldap_cn": null,
"ldap_access": null
}
+ ],
+ "contains_hidden_groups": false
+ }
+]
+```
+
+### Create project-level rule
+
+>**Note:** This API endpoint is only available on 12.3 Starter and above.
+
+You can create project approval rules using the following endpoint:
+
+```
+POST /projects/:id/approval_rules
+```
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|----------------------|---------|----------|-----------------------------------------------------------|
+| `id` | integer | yes | The ID of a project |
+| `name` | string | yes | The name of the approval rule |
+| `approvals_required` | integer | yes | The number of required approvals for this rule |
+| `users` | integer | no | The ids of users as approvers |
+| `groups` | integer | no | The ids of groups as approvers |
+
+```json
+{
+ "id": 1,
+ "name": "security",
+ "rule_type": "regular",
+ "eligible_approvers": [
+ {
+ "id": 2,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ },
+ {
+ "id": 50,
+ "name": "Group Member 1",
+ "username": "group_member_1",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/group_member_1"
}
],
- "approvals_before_merge": 2,
- "reset_approvals_on_push": true,
- "disable_overriding_approvers_per_merge_request": false,
- "merge_requests_author_approval": false,
- "merge_requests_disable_committers_approval": false
+ "approvals_required": 1,
+ "users": [
+ {
+ "id": 2,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ }
+ ],
+ "groups": [
+ {
+ "id": 5,
+ "name": "group1",
+ "path": "group1",
+ "description": "",
+ "visibility": "public",
+ "lfs_enabled": false,
+ "avatar_url": null,
+ "web_url": "http://localhost/groups/group1",
+ "request_access_enabled": false,
+ "full_name": "group1",
+ "full_path": "group1",
+ "parent_id": null,
+ "ldap_cn": null,
+ "ldap_access": null
+ }
+ ],
+ "contains_hidden_groups": false
+}
+```
+
+### Update project-level rule
+
+>**Note:** This API endpoint is only available on 12.3 Starter and above.
+
+You can update project approval rules using the following endpoint:
+
+```
+PUT /projects/:id/approval_rules/:approval_rule_id
+```
+
+**Important:** Approvers and groups not in the `users`/`groups` param will be **removed**
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|----------------------|---------|----------|-----------------------------------------------------------|
+| `id` | integer | yes | The ID of a project |
+| `approval_rule_id` | integer | yes | The ID of a approval rule |
+| `name` | string | yes | The name of the approval rule |
+| `approvals_required` | integer | yes | The number of required approvals for this rule |
+| `users` | integer | no | The ids of users as approvers |
+| `groups` | integer | no | The ids of groups as approvers |
+
+```json
+{
+ "id": 1,
+ "name": "security",
+ "rule_type": "regular",
+ "eligible_approvers": [
+ {
+ "id": 2,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ },
+ {
+ "id": 50,
+ "name": "Group Member 1",
+ "username": "group_member_1",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/group_member_1"
+ }
+ ],
+ "approvals_required": 1,
+ "users": [
+ {
+ "id": 2,
+ "name": "John Doe",
+ "username": "jdoe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon",
+ "web_url": "http://localhost/jdoe"
+ }
+ ],
+ "groups": [
+ {
+ "id": 5,
+ "name": "group1",
+ "path": "group1",
+ "description": "",
+ "visibility": "public",
+ "lfs_enabled": false,
+ "avatar_url": null,
+ "web_url": "http://localhost/groups/group1",
+ "request_access_enabled": false,
+ "full_name": "group1",
+ "full_path": "group1",
+ "parent_id": null,
+ "ldap_cn": null,
+ "ldap_access": null
+ }
+ ],
+ "contains_hidden_groups": false
}
```
+### Delete project-level rule
+
+>**Note:** This API endpoint is only available on 12.3 Starter and above.
+
+You can delete project approval rules using the following endpoint:
+
+```
+DELETE /projects/:id/approval_rules/:approval_rule_id
+```
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|----------------------|---------|----------|-----------------------------------------------------------|
+| `id` | integer | yes | The ID of a project |
+| `approval_rule_id` | integer | yes | The ID of a approval rule
+
### Change allowed approvers
+>**Note:** This API endpoint has been deprecated. Please use Approval Rule API instead.
>**Note:** This API endpoint is only available on 10.6 Starter and above.
If you are allowed to, you can change approvers and approver groups using
@@ -227,8 +414,6 @@ GET /projects/:id/merge_requests/:merge_request_iid/approvals
}
}
],
- "approvers": [],
- "approver_groups": []
}
```
@@ -249,7 +434,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/approvals
|----------------------|---------|----------|--------------------------------------------|
| `id` | integer | yes | The ID of a project |
| `merge_request_iid` | integer | yes | The IID of MR |
-| `approvals_required` | integer | yes | Approvals required before MR can be merged |
+| `approvals_required` | integer | yes | Approvals required before MR can be merged. Deprecated in 12.0 in favor of Approval Rules API. |
```json
{
@@ -264,14 +449,13 @@ POST /projects/:id/merge_requests/:merge_request_iid/approvals
"merge_status": "cannot_be_merged",
"approvals_required": 2,
"approvals_left": 2,
- "approved_by": [],
- "approvers": [],
- "approver_groups": []
+ "approved_by": []
}
```
### Change allowed approvers for Merge Request
+>**Note:** This API endpoint has been deprecated. Please use Approval Rule API instead.
>**Note:** This API endpoint is only available on 10.6 Starter and above.
If you are allowed to, you can change approvers and approver groups using
@@ -401,8 +585,6 @@ does not match, the response code will be `409`.
}
}
],
- "approvers": [],
- "approver_groups": []
}
```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 1ade46efb1c..49ed4968b0d 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -1127,7 +1127,7 @@ the `approvals_before_merge` parameter:
## Delete a merge request
-Only for admins and project owners. Soft deletes the merge request in question.
+Only for admins and project owners. Deletes the merge request in question.
```
DELETE /projects/:id/merge_requests/:merge_request_iid
diff --git a/doc/ci/directed_acyclic_graph/index.md b/doc/ci/directed_acyclic_graph/index.md
index e54be9f3bd9..1c38c08b7cb 100644
--- a/doc/ci/directed_acyclic_graph/index.md
+++ b/doc/ci/directed_acyclic_graph/index.md
@@ -65,7 +65,7 @@ as quickly as possible.
Relationships are defined between jobs using the [`needs:` keyword](../yaml/README.md#needs).
Note that `needs:` also works with the [parallel](../yaml/README.md#parallel) keyword,
-giving your powerful options for parallelization within your pipeline.
+giving you powerful options for parallelization within your pipeline.
## Limitations
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index d5056568dff..489791141ed 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -495,14 +495,6 @@ that runner.
## Define an image from a private Container Registry
-> **Notes:**
->
-> - This feature requires GitLab Runner **1.8** or higher
-> - For GitLab Runner versions **>= 0.6, <1.8** there was a partial
-> support for using private registries, which required manual configuration
-> of credentials on runner's host. We recommend to upgrade your Runner to
-> at least version **1.8** if you want to use private registries.
-
To access private container registries, the GitLab Runner process can use:
- [Statically defined credentials](#using-statically-defined-credentials). That is, a username and password for a specific registry.
@@ -525,6 +517,17 @@ it's provided as an environment variable. This is because GitLab Runnner uses **
`config.toml` configuration and doesn't interpolate **ANY** environment variables at
runtime.
+### Requirements and limitations
+
+- This feature requires GitLab Runner **1.8** or higher.
+- For GitLab Runner versions **>= 0.6, <1.8** there was a partial
+ support for using private registries, which required manual configuration
+ of credentials on runner's host. We recommend to upgrade your Runner to
+ at least version **1.8** if you want to use private registries.
+- Not available for [Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html),
+ follow <https://gitlab.com/gitlab-org/gitlab-runner/issues/2673> for
+ details.
+
### Using statically-defined credentials
There are two approaches that you can take in order to access a
diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
index ad07c662965..126e12e460f 100644
--- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
@@ -35,10 +35,9 @@ get out of WIP status or resolve merge conflicts as soon as possible.
## Requirements and limitations
-Pipelines for merged results require:
+Pipelines for merged results require a [GitLab Runner][runner] 11.9 or newer.
-- [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer.
-- [Gitaly](https://gitlab.com/gitlab-org/gitaly) 1.21.0 or newer.
+[runner]: https://gitlab.com/gitlab-org/gitlab-runner
In addition, pipelines for merged results have the following limitations:
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index d9f022a7125..b6aebd3bd78 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -76,7 +76,7 @@ to access it. This is where an SSH key pair comes in handy.
## without extra base64 encoding.
## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556
##
- - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
+ - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
##
## Create the SSH directory and give it the right permissions
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index d741482b662..5a15b907da0 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -94,7 +94,10 @@ This means that the value of the variable will be hidden in job logs,
though it must match certain requirements to do so:
- The value must be in a single line.
-- The value must only consist of characters from the Base64 alphabet ([RFC4648](https://tools.ietf.org/html/rfc4648)) with the addition of `@` and `:`.
+- The value must only consist of characters from the Base64 alphabet (RFC4648).
+
+ [In GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-ce/issues/63043)
+ and newer, `@` and `:` are also valid values.
- The value must be at least 8 characters long.
- The value must not use variables.
@@ -509,7 +512,7 @@ Below you can find supported syntax reference:
1. Checking for an empty variable
Examples:
-
+
- `$VARIABLE == ""`
- `$VARIABLE != ""` (introduced in GitLab 11.11)
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 2be93433b36..f7a67931793 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -518,10 +518,24 @@ Four keys are available:
- `changes`
- `kubernetes`
-If you use multiple keys under `only` or `except`, they act as an AND. The logic is:
+If you use multiple keys under `only` or `except`, the keys will be evaluated as a
+single conjoined expression. That is:
+
+- `only:` means "include this job if all of the conditions match".
+- `except:` means "exclude this job if any of the conditions match".
+
+With `only`, individual keys are logically joined by an AND:
> (any of refs) AND (any of variables) AND (any of changes) AND (if kubernetes is active)
+`except` is implemented as a negation of this complete expression:
+
+> NOT((any of refs) AND (any of variables) AND (any of changes) AND (if kubernetes is active))
+
+This, more intuitively, means the keys join by an OR. A functionally equivalent expression:
+
+> (any of refs) OR (any of variables) OR (any of changes) OR (if kubernetes is active)
+
#### `only:refs`/`except:refs`
> `refs` policy introduced in GitLab 10.0.
@@ -1721,7 +1735,7 @@ This example creates three paths of execution:
1. If `needs:` is set to point to a job that is not instantiated
because of `only/except` rules or otherwise does not exist, the
job will fail.
-1. Note that one day one of the launch, we are temporarily limiting the
+1. Note that on day one of the launch, we are temporarily limiting the
maximum number of jobs that a single job can need in the `needs:` array. Track
our [infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/7541)
for details on the current limit.
@@ -1734,8 +1748,8 @@ This example creates three paths of execution:
in the first stage (see [gitlab-ce#65504](https://gitlab.com/gitlab-org/gitlab-ce/issues/65504)).
1. If `needs:` refers to a job that is marked as `parallel:`.
the current job will depend on all parallel jobs created.
-1. `needs:` is similar to `dependencies:` in that needs to use jobs from
- prior stages, this means that it is impossible to create circular
+1. `needs:` is similar to `dependencies:` in that it needs to use jobs from
+ prior stages, meaning it is impossible to create circular
dependencies or depend on jobs in the current stage (see [gitlab-ce#65505](https://gitlab.com/gitlab-org/gitlab-ce/issues/65505)).
1. Related to the above, stages must be explicitly defined for all jobs
that have the keyword `needs:` or are referred to by one.
diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md
index 98b8a48abf4..001a92790e1 100644
--- a/doc/development/automatic_ce_ee_merge.md
+++ b/doc/development/automatic_ce_ee_merge.md
@@ -200,7 +200,7 @@ code.
### Why merge automatically?
As we work towards continuous deployments and a single repository for both CE
-and EE, we need to first make sure that all CE changes make their way into CE as
+and EE, we need to first make sure that all CE changes make their way into EE as
fast as possible. Past experiences and data have shown that periodic CE to EE
merge requests do not scale, and often take a very long time to complete. For
example, [in this
diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md
index 635895051bc..090e5235619 100644
--- a/doc/development/elasticsearch.md
+++ b/doc/development/elasticsearch.md
@@ -148,26 +148,49 @@ Uses an [Edge NGram token filter](https://www.elastic.co/guide/en/elasticsearch/
- Searches can have their own analyzers. Remember to check when editing analyzers
- `Character` filters (as opposed to token filters) always replace the original character, so they're not a good choice as they can hinder exact searches
-## Architecture
+## Zero downtime reindexing with multiple indices
-GitLab uses `elasticsearch-rails` for handling communication with Elasticsearch server. However, in order to achieve zero-downtime deployment during schema changes, an extra abstraction layer is built to allow:
+Currently GitLab can only handle a single version of setting. Any setting/schema changes would require reindexing everything from scratch. Since reindexing can take a long time, this can cause search functionality downtime.
-* Indexing (writes) to multiple indexes, with different mappings
-* Switching to different index for searches (reads) on the fly
+To avoid downtime, GitLab is working to support multiple indices that
+can function at the same time. Whenever the schema changes, the admin
+will be able to create a new index and reindex to it, while searches
+continue to go to the older, stable index. Any data updates will be
+forwarded to both indices. Once the new index is ready, an admin can
+mark it active, which will direct all searches to it, and remove the old
+index.
-Currently we are on the process of migrating models to this new design (e.g. `Snippet`), and it is hardwired to work with a single version for now.
+This is also helpful for migrating to new servers, e.g. moving to/from AWS.
-Traditionally, `elasticsearch-rails` provides class and instance level `__elasticsearch__` proxy methods. If you call `Issue.__elasticsearch__`, you will get an instance of `Elasticsearch::Model::Proxy::ClassMethodsProxy`, and if you call `Issue.first.__elasticsearch__`, you will get an instance of `Elasticsearch::Model::Proxy::InstanceMethodsProxy`. These proxy objects would talk to Elasticsearch server directly.
+Currently we are on the process of migrating to this new design. Everything is hardwired to work with one single version for now.
-In the new design, `__elasticsearch__` instead represents one extra layer of proxy. It would keep multiple versions of the actual proxy objects, and it would forward read and write calls to the proxy of the intended version.
+### Architecture
-The `elasticsearch-rails`'s way of specifying each model's mappings and other settings is to create a module for the model to include. However in the new design, each model would have its own corresponding subclassed proxy object, where the settings reside in. For example, snippet related setting in the past reside in `SnippetsSearch` module, but in the new design would reside in `SnippetClassProxy` (which is a subclass of `Elasticsearch::Model::Proxy::ClassMethodsProxy`). This reduces namespace pollution in model classes.
+The traditional setup, provided by `elasticsearch-rails`, is to communicate through its internal proxy classes. Developers would write model-specific logic in a module for the model to include in (e.g. `SnippetsSearch`). The `__elasticsearch__` methods would return a proxy object, e.g.:
+
+- `Issue.__elasticsearch__` returns an instance of `Elasticsearch::Model::Proxy::ClassMethodsProxy`
+- `Issue.first.__elasticsearch__` returns an instance of `Elasticsearch::Model::Proxy::InstanceMethodsProxy`.
+
+These proxy objects would talk to Elasticsearch server directly (see top half of the diagram).
+
+![Elasticsearch Architecture](img/elasticsearch_architecture.svg)
+
+In the planned new design, each model would have a pair of corresponding subclassed proxy objects, in which model-specific logic is located. For example, `Snippet` would have `SnippetClassProxy` and `SnippetInstanceProxy` (being subclass of `Elasticsearch::Model::Proxy::ClassMethodsProxy` and `Elasticsearch::Model::Proxy::InstanceMethodsProxy`, respectively).
+
+`__elasticsearch__` would represent another layer of proxy object, keeping track of multiple actual proxy objects. It would forward method calls to the appropriate index. For example:
+
+- `model.__elasticsearch__.search` would be forwarded to the one stable index, since it is a read operation.
+- `model.__elasticsearch__.update_document` would be forwarded to all indices, to keep all indices up-to-date.
The global configurations per version are now in the `Elastic::(Version)::Config` class. You can change mappings there.
### Creating new version of schema
-Currently GitLab would still work with a single version of setting. Once it is implemented, multiple versions of setting can exists in different folders (e.g. `ee/lib/elastic/v12p1` and `ee/lib/elastic/v12p3`). To keep a continuous git history, the latest version lives under the `/latest` folder, but is aliased as the latest version.
+NOTE: **Note:** this is not applicable yet as multiple indices functionality is not fully implemented.
+
+Folders like `ee/lib/elastic/v12p1` contain snapshots of search logic from different versions. To keep a continuous git history, the latest version lives under `ee/lib/elastic/latest`, but its classes are aliased under an actual version (e.g. `ee/lib/elastic/v12p3`). When referencing these classes, never use the `Latest` namespace directly, but use the actual version (e.g. `V12p3`).
+
+The version name basically follows GitLab's release version. If setting is changed in 12.3, we will create a new namespace called `V12p3` (p stands for "point"). Raise an issue if there is a need to name a version differently.
If the current version is `v12p1`, and we need to create a new version for `v12p3`, the steps are as follows:
@@ -176,7 +199,7 @@ If the current version is `v12p1`, and we need to create a new version for `v12p
1. Delete `v12p1` folder
1. Copy the entire folder of `latest` as `v12p1`
1. Change the namespace for files under `v12p1` folder from `Latest` to `V12p1`
-1. Make changes to `Latest` as needed
+1. Make changes to files under the `latest` folder as needed
## Troubleshooting
diff --git a/doc/development/fe_guide/axios.md b/doc/development/fe_guide/axios.md
index 09b4a3c3d96..6e7cf523f36 100644
--- a/doc/development/fe_guide/axios.md
+++ b/doc/development/fe_guide/axios.md
@@ -38,7 +38,7 @@ Advantages over [`spyOn()`]:
- no need to create response objects
- does not allow call through (which we want to avoid)
-- simple API to test error cases
+- simple API to test error cases
- provides `replyOnce()` to allow for different responses
We have also decided against using [axios interceptors] because they are not suitable for mocking.
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index d3fa350b847..125b11afcd0 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -21,31 +21,31 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
1. **Never Ever EVER** disable eslint globally for a file
```javascript
- // bad
- /* eslint-disable */
+ // bad
+ /* eslint-disable */
- // better
- /* eslint-disable some-rule, some-other-rule */
+ // better
+ /* eslint-disable some-rule, some-other-rule */
- // best
- // nothing :)
+ // best
+ // nothing :)
```
1. If you do need to disable a rule for a single violation, try to do it as locally as possible
```javascript
- // bad
- /* eslint-disable no-new */
+ // bad
+ /* eslint-disable no-new */
- import Foo from 'foo';
+ import Foo from 'foo';
- new Foo();
+ new Foo();
- // better
- import Foo from 'foo';
+ // better
+ import Foo from 'foo';
- // eslint-disable-next-line no-new
- new Foo();
+ // eslint-disable-next-line no-new
+ new Foo();
```
1. There are few rules that we need to disable due to technical debt. Which are:
@@ -56,16 +56,16 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
followed by any global declarations, then a blank newline prior to any imports or code.
```javascript
- // bad
- /* global Foo */
- /* eslint-disable no-new */
- import Bar from './bar';
+ // bad
+ /* global Foo */
+ /* eslint-disable no-new */
+ import Bar from './bar';
- // good
- /* eslint-disable no-new */
- /* global Foo */
+ // good
+ /* eslint-disable no-new */
+ /* global Foo */
- import Bar from './bar';
+ import Bar from './bar';
```
1. **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
@@ -73,23 +73,23 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
1. When declaring multiple globals, always use one `/* global [name] */` line per variable.
```javascript
- // bad
- /* globals Flash, Cookies, jQuery */
+ // bad
+ /* globals Flash, Cookies, jQuery */
- // good
- /* global Flash */
- /* global Cookies */
- /* global jQuery */
+ // good
+ /* global Flash */
+ /* global Cookies */
+ /* global jQuery */
```
1. Use up to 3 parameters for a function or class. If you need more accept an Object instead.
```javascript
- // bad
- fn(p1, p2, p3, p4) {}
+ // bad
+ fn(p1, p2, p3, p4) {}
- // good
- fn(options) {}
+ // good
+ fn(options) {}
```
#### Modules, Imports, and Exports
@@ -97,32 +97,32 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
1. Use ES module syntax to import modules
```javascript
- // bad
- const SomeClass = require('some_class');
+ // bad
+ const SomeClass = require('some_class');
- // good
- import SomeClass from 'some_class';
+ // good
+ import SomeClass from 'some_class';
- // bad
- module.exports = SomeClass;
+ // bad
+ module.exports = SomeClass;
- // good
- export default SomeClass;
+ // good
+ export default SomeClass;
```
Import statements are following usual naming guidelines, for example object literals use camel case:
```javascript
- // some_object file
- export default {
- key: 'value',
- };
+ // some_object file
+ export default {
+ key: 'value',
+ };
- // bad
- import ObjectLiteral from 'some_object';
+ // bad
+ import ObjectLiteral from 'some_object';
- // good
- import objectLiteral from 'some_object';
+ // good
+ import objectLiteral from 'some_object';
```
1. Relative paths: when importing a module in the same directory, a child
@@ -171,22 +171,22 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
1. Avoid adding to the global namespace.
```javascript
- // bad
- window.MyClass = class { /* ... */ };
+ // bad
+ window.MyClass = class { /* ... */ };
- // good
- export default class MyClass { /* ... */ }
+ // good
+ export default class MyClass { /* ... */ }
```
1. Side effects are forbidden in any script which contains export
```javascript
- // bad
- export default class MyClass { /* ... */ }
+ // bad
+ export default class MyClass { /* ... */ }
- document.addEventListener("DOMContentLoaded", function(event) {
- new MyClass();
- }
+ document.addEventListener("DOMContentLoaded", function(event) {
+ new MyClass();
+ }
```
#### Data Mutation and Pure functions
@@ -257,17 +257,17 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
`.reduce` or `.filter`
```javascript
- const users = [ { name: 'Foo' }, { name: 'Bar' } ];
+ const users = [ { name: 'Foo' }, { name: 'Bar' } ];
- // bad
- users.forEach((user, index) => {
- user.id = index;
- });
+ // bad
+ users.forEach((user, index) => {
+ user.id = index;
+ });
- // good
- const usersWithId = users.map((user, index) => {
- return Object.assign({}, user, { id: index });
- });
+ // good
+ const usersWithId = users.map((user, index) => {
+ return Object.assign({}, user, { id: index });
+ });
```
#### Parse Strings into Numbers
@@ -275,14 +275,14 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
1. `parseInt()` is preferable over `Number()` or `+`
```javascript
- // bad
- +'10' // 10
+ // bad
+ +'10' // 10
- // good
- Number('10') // 10
+ // good
+ Number('10') // 10
- // better
- parseInt('10', 10);
+ // better
+ parseInt('10', 10);
```
#### CSS classes used for JavaScript
@@ -290,15 +290,15 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/
1. If the class is being used in Javascript it needs to be prepend with `js-`
```html
- // bad
- <button class="add-user">
- Add User
- </button>
+ // bad
+ <button class="add-user">
+ Add User
+ </button>
- // good
- <button class="js-add-user">
- Add User
- </button>
+ // good
+ <button class="js-add-user">
+ Add User
+ </button>
```
### Vue.js
@@ -315,41 +315,41 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
1. Use a function in the bundle file to instantiate the Vue component:
```javascript
- // bad
- class {
- init() {
- new Component({})
- }
+ // bad
+ class {
+ init() {
+ new Component({})
}
+ }
- // good
- document.addEventListener('DOMContentLoaded', () => new Vue({
- el: '#element',
- components: {
- componentName
- },
- render: createElement => createElement('component-name'),
- }));
+ // good
+ document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#element',
+ components: {
+ componentName
+ },
+ render: createElement => createElement('component-name'),
+ }));
```
1. Do not use a singleton for the service or the store
```javascript
- // bad
- class Store {
- constructor() {
- if (!this.prototype.singleton) {
- // do something
- }
+ // bad
+ class Store {
+ constructor() {
+ if (!this.prototype.singleton) {
+ // do something
}
}
+ }
- // good
- class Store {
- constructor() {
- // do something
- }
+ // good
+ class Store {
+ constructor() {
+ // do something
}
+ }
```
1. Use `.vue` for Vue templates. Do not use `%template` in HAML.
@@ -360,36 +360,36 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
1. **Reference Naming**: Use PascalCase for their instances:
```javascript
- // bad
- import cardBoard from 'cardBoard.vue'
+ // bad
+ import cardBoard from 'cardBoard.vue'
- components: {
- cardBoard,
- };
+ components: {
+ cardBoard,
+ };
- // good
- import CardBoard from 'cardBoard.vue'
+ // good
+ import CardBoard from 'cardBoard.vue'
- components: {
- CardBoard,
- };
+ components: {
+ CardBoard,
+ };
```
1. **Props Naming:** Avoid using DOM component prop names.
1. **Props Naming:** Use kebab-case instead of camelCase to provide props in templates.
```javascript
- // bad
- <component class="btn">
+ // bad
+ <component class="btn">
- // good
- <component css-class="btn">
+ // good
+ <component css-class="btn">
- // bad
- <component myProp="prop" />
+ // bad
+ <component myProp="prop" />
- // good
- <component my-prop="prop" />
+ // good
+ <component my-prop="prop" />
```
[#34371]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34371
@@ -401,37 +401,37 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
1. With more than one attribute, all attributes should be on a new line:
```javascript
- // bad
- <component v-if="bar"
- param="baz" />
+ // bad
+ <component v-if="bar"
+ param="baz" />
- <button class="btn">Click me</button>
+ <button class="btn">Click me</button>
- // good
- <component
- v-if="bar"
- param="baz"
- />
+ // good
+ <component
+ v-if="bar"
+ param="baz"
+ />
- <button class="btn">
- Click me
- </button>
+ <button class="btn">
+ Click me
+ </button>
```
1. The tag can be inline if there is only one attribute:
```javascript
- // good
- <component bar="bar" />
+ // good
+ <component bar="bar" />
- // good
- <component
- bar="bar"
- />
+ // good
+ <component
+ bar="bar"
+ />
- // bad
- <component
- bar="bar" />
+ // bad
+ <component
+ bar="bar" />
```
#### Quotes
@@ -439,15 +439,15 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
1. Always use double quotes `"` inside templates and single quotes `'` for all other JS.
```javascript
- // bad
- template: `
- <button :class='style'>Button</button>
- `
+ // bad
+ template: `
+ <button :class='style'>Button</button>
+ `
- // good
- template: `
- <button :class="style">Button</button>
- `
+ // good
+ template: `
+ <button :class="style">Button</button>
+ `
```
#### Props
@@ -455,37 +455,37 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
1. Props should be declared as an object
```javascript
- // bad
- props: ['foo']
-
- // good
- props: {
- foo: {
- type: String,
- required: false,
- default: 'bar'
- }
+ // bad
+ props: ['foo']
+
+ // good
+ props: {
+ foo: {
+ type: String,
+ required: false,
+ default: 'bar'
}
+ }
```
1. Required key should always be provided when declaring a prop
```javascript
- // bad
- props: {
- foo: {
- type: String,
- }
+ // bad
+ props: {
+ foo: {
+ type: String,
}
+ }
- // good
- props: {
- foo: {
- type: String,
- required: false,
- default: 'bar'
- }
+ // good
+ props: {
+ foo: {
+ type: String,
+ required: false,
+ default: 'bar'
}
+ }
```
1. Default key should be provided if the prop is not required.
@@ -493,30 +493,30 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
On those a default key should not be provided.
```javascript
- // good
- props: {
- foo: {
- type: String,
- required: false,
- }
+ // good
+ props: {
+ foo: {
+ type: String,
+ required: false,
}
+ }
- // good
- props: {
- foo: {
- type: String,
- required: false,
- default: 'bar'
- }
+ // good
+ props: {
+ foo: {
+ type: String,
+ required: false,
+ default: 'bar'
}
+ }
- // good
- props: {
- foo: {
- type: String,
- required: true
- }
+ // good
+ props: {
+ foo: {
+ type: String,
+ required: true
}
+ }
```
#### Data
@@ -524,17 +524,17 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
1. `data` method should always be a function
```javascript
- // bad
- data: {
- foo: 'foo'
- }
+ // bad
+ data: {
+ foo: 'foo'
+ }
- // good
- data() {
- return {
- foo: 'foo'
- };
- }
+ // good
+ data() {
+ return {
+ foo: 'foo'
+ };
+ }
```
#### Directives
@@ -542,31 +542,31 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
1. Shorthand `@` is preferable over `v-on`
```javascript
- // bad
- <component v-on:click="eventHandler"/>
+ // bad
+ <component v-on:click="eventHandler"/>
- // good
- <component @click="eventHandler"/>
+ // good
+ <component @click="eventHandler"/>
```
1. Shorthand `:` is preferable over `v-bind`
```javascript
- // bad
- <component v-bind:class="btn"/>
+ // bad
+ <component v-bind:class="btn"/>
- // good
- <component :class="btn"/>
+ // good
+ <component :class="btn"/>
```
1. Shorthand `#` is preferable over `v-slot`
```javascript
- // bad
- <template v-slot:header></template>
+ // bad
+ <template v-slot:header></template>
- // good
- <template #header></template>
+ // good
+ <template #header></template>
```
#### Closing tags
@@ -574,11 +574,11 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation.
1. Prefer self closing component tags
```javascript
- // bad
- <component></component>
+ // bad
+ <component></component>
- // good
- <component />
+ // good
+ <component />
```
#### Ordering
@@ -610,48 +610,48 @@ When using `v-for` you need to provide a *unique* `:key` attribute for each item
1. If the elements of the array being iterated have an unique `id` it is advised to use it:
```html
- <div
- v-for="item in items"
- :key="item.id"
- >
- <!-- content -->
- </div>
+ <div
+ v-for="item in items"
+ :key="item.id"
+ >
+ <!-- content -->
+ </div>
```
1. When the elements being iterated don't have a unique id, you can use the array index as the `:key` attribute
```html
- <div
- v-for="(item, index) in items"
- :key="index"
- >
- <!-- content -->
- </div>
+ <div
+ v-for="(item, index) in items"
+ :key="index"
+ >
+ <!-- content -->
+ </div>
```
1. When using `v-for` with `template` and there is more than one child element, the `:key` values must be unique. It's advised to use `kebab-case` namespaces.
```html
- <template v-for="(item, index) in items">
- <span :key="`span-${index}`"></span>
- <button :key="`button-${index}`"></button>
- </template>
+ <template v-for="(item, index) in items">
+ <span :key="`span-${index}`"></span>
+ <button :key="`button-${index}`"></button>
+ </template>
```
1. When dealing with nested `v-for` use the same guidelines as above.
```html
- <div
- v-for="item in items"
- :key="item.id"
+ <div
+ v-for="item in items"
+ :key="item.id"
+ >
+ <span
+ v-for="element in array"
+ :key="element.id"
>
- <span
- v-for="element in array"
- :key="element.id"
- >
- <!-- content -->
- </span>
- </div>
+ <!-- content -->
+ </span>
+ </div>
```
Useful links:
@@ -664,19 +664,19 @@ Useful links:
1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
```javascript
- // bad
- <span
- class="has-tooltip"
- title="Some tooltip text">
- Text
- </span>
+ // bad
+ <span
+ class="has-tooltip"
+ title="Some tooltip text">
+ Text
+ </span>
- // good
- <span
- v-tooltip
- title="Some tooltip text">
- Text
- </span>
+ // good
+ <span
+ v-tooltip
+ title="Some tooltip text">
+ Text
+ </span>
```
1. Tooltips: When using a tooltip, include the tooltip directive, `./app/assets/javascripts/vue_shared/directives/tooltip.js`
@@ -684,13 +684,13 @@ Useful links:
1. Don't change `data-original-title`.
```javascript
- // bad
- <span data-original-title="tooltip text">Foo</span>
+ // bad
+ <span data-original-title="tooltip text">Foo</span>
- // good
- <span title="tooltip text">Foo</span>
+ // good
+ <span title="tooltip text">Foo</span>
- $('span').tooltip('_fixTitle');
+ $('span').tooltip('_fixTitle');
```
### The Javascript/Vue Accord
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 9eeaee4482f..557d3132d71 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -313,7 +313,7 @@ export default {
1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency throughout the application. From Vuex docs:
- > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action.
+ > Why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action.
```javascript
// component.vue
diff --git a/doc/development/img/elasticsearch_architecture.svg b/doc/development/img/elasticsearch_architecture.svg
new file mode 100644
index 00000000000..2f38f9b04ee
--- /dev/null
+++ b/doc/development/img/elasticsearch_architecture.svg
@@ -0,0 +1 @@
+<svg version="1.2" width="210mm" height="297mm" viewBox="0 0 21000 29700" preserveAspectRatio="xMidYMid" fill-rule="evenodd" stroke-width="28.222" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><defs class="ClipPathGroup"><clipPath id="a" clipPathUnits="userSpaceOnUse"><path d="M0 0h21000v29700H0z"/></clipPath></defs><g class="SlideGroup"><g class="Slide" clip-path="url(#a)"><g class="Page"><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 5575h3051v1651H1975z"/><path fill="#FFF" d="M3500 7200H2000V5600h3000v1600H3500z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3500 7200H2000V5600h3000v1600H3500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2778" y="6311"><tspan>Snippet</tspan></tspan><tspan class="TextPosition" x="2099" y="6785"><tspan>(ActiveRecord)</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1475 3975h4051v3551H1475z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3500 7500H1500V4000h4000v3500H3500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="1788" y="5048"><tspan>ApplicationSearch</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M5975 4675h8051v701H5975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M6000 5350h4000v-650h4000"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M5975 5325h8051v1101H5975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M6000 5350h4000v1050h4000"/></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1075 2875h4951v4951H1075z"/><path fill="none" stroke="#F33" stroke-width="50" d="M3550 7800H1100V2900h4900v4900H3550z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="1946" y="3514"><tspan fill="#C9211E">SnippetsSearch</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 12175h3051v1651H1975z"/><path fill="#FFF" d="M3500 13800H2000v-1600h3000v1600H3500z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3500 13800H2000v-1600h3000v1600H3500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2778" y="12911"><tspan>Snippet</tspan></tspan><tspan class="TextPosition" x="2099" y="13385"><tspan>(ActiveRecord)</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1075 10775h4951v3251H1075z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3550 14000H1100v-3200h4900v3200H3550z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2511" y="11461"><tspan>Application</tspan></tspan><tspan class="TextPosition" x="1933" y="11935"><tspan>VersionedSearch</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M3525 13975h4501v7451H3525z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3550 14000v7400h4450"/></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 14075h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 14900h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14720" y="14648"><tspan fill="gray">ClassMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 13075h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 15200h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13799" y="13731"><tspan fill="#C9211E">V12p1::SnippetClassProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M7975 14575h3051v1851H7975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M9500 16400H8000v-1800h3000v1800H9500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="8277" y="15411"><tspan>MultiVersion-</tspan></tspan><tspan class="TextPosition" x="8429" y="15885"><tspan>ClassProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 16875h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 17700h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14720" y="17448"><tspan fill="gray">ClassMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 15875h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 18000h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13799" y="16531"><tspan fill="#C9211E">V12p2::SnippetClassProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 14125h2451v1401h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 15500h1463v-1350h937"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 15475h2451v1501h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 15500h1463v1450h937"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M3525 13975h4501v1551H3525z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3550 14000v1500h4450"/></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 19975h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 20800h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14445" y="20548"><tspan fill="gray">InstanceMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 18975h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 21100h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13505" y="19631"><tspan fill="#C9211E">V12p1::SnippetInstanceProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M7975 20275h3051v2251H7975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M9500 22500H8000v-2200h3000v2200H9500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="8277" y="21311"><tspan>MultiVersion-</tspan></tspan><tspan class="TextPosition" x="8154" y="21785"><tspan>InstanceProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 22775h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 23600h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14445" y="23348"><tspan fill="gray">InstanceMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 21775h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 23900h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13505" y="22431"><tspan fill="#C9211E">V12p2::SnippetInstanceProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 20025h2451v1401h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 21400h1463v-1350h937"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 21375h2451v1501h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 21400h1463v1450h937"/></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M900 1600h10697v879H900z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="564" font-weight="400"><tspan class="TextPosition" x="1150" y="2233"><tspan>Standard elasticsearch-rails setup</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M900 9300h7683v879H900z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="564" font-weight="400"><tspan class="TextPosition" x="1150" y="9933"><tspan>GitLab multi-indices setup</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M3400 21300h4821v1197H3400z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="4250" y="21840"><tspan fill="gray">(instance method)</tspan></tspan><tspan class="TextPosition" x="3651" y="22264"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M3380 15400h4821v1197H3380z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="4512" y="15940"><tspan fill="gray">(class method)</tspan></tspan><tspan class="TextPosition" x="3631" y="16364"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M9000 3500h4821v1197H9000z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="10132" y="4040"><tspan fill="gray">(class method)</tspan></tspan><tspan class="TextPosition" x="9251" y="4464"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M9000 6400h4821v1197H9000z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="9850" y="6940"><tspan fill="gray">(instance method)</tspan></tspan><tspan class="TextPosition" x="9251" y="7364"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 25175h2051v851H1975z"/><path fill="none" stroke="#999" stroke-width="50" d="M3000 26000H2000v-800h2000v800H3000z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2634" y="25748"><tspan fill="gray">Foo</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M4400 25200h7101v726H4400z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="4650" y="25710"><tspan>elasticsearch-rails’ internal class</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M4400 26400h8601v1200H4400z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="4650" y="26910"><tspan>where model-specific logic is</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 26275h2051v851H1975z"/><path fill="none" stroke="#F33" stroke-width="50" d="M3000 27100H2000v-800h2000v800H3000z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="2613" y="26848"><tspan fill="#C9211E">Foo</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M4900 17289h5901v2312H4900z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="370" font-weight="400"><tspan class="TextPosition" x="7236" y="17748"><tspan fill="gray">Write operations like </tspan></tspan><tspan class="TextPosition" x="5323" y="18159"><tspan fill="gray">indexing/updating are forwarded </tspan></tspan><tspan class="TextPosition" x="8024" y="18570"><tspan fill="gray">to all instances.</tspan></tspan></tspan><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="370" font-weight="400"><tspan class="TextPosition" x="5501" y="18981"><tspan fill="gray">Read operations are forwarded </tspan></tspan><tspan class="TextPosition" x="7126" y="19392"><tspan fill="gray">to specified instance.</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10785 15769h1422v2691h-1422z"/><path fill="none" stroke="#999" stroke-width="30" d="M10800 18444c1429 0 934-1618 1119-2337"/><path fill="#999" d="M12206 15769l-460 293 267 217 193-510z"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10785 18429h1528v2862h-1528z"/><path fill="none" stroke="#999" stroke-width="30" d="M10800 18444c1509 0 970 1782 1200 2526"/><path fill="#999" d="M12312 21290l-227-496-252 235 479 261z"/></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M1800 24000h7101v807H1800z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="494" font-weight="700"><tspan class="TextPosition" x="2050" y="24574"><tspan>Legend</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13975 4275h5085v851h-5085z"/><path fill="none" stroke="#999" stroke-width="50" d="M16517 5100h-2517v-800h5034v800h-2517z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14737" y="4848"><tspan fill="gray">ClassMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13975 5975h5085v851h-5085z"/><path fill="none" stroke="#999" stroke-width="50" d="M16517 6800h-2517v-800h5034v800h-2517z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14462" y="6548"><tspan fill="gray">InstanceMethodProxy</tspan></tspan></tspan></text></g></g></g></g></svg> \ No newline at end of file
diff --git a/doc/development/interacting_components.md b/doc/development/interacting_components.md
index 74d52d808e2..5e6dc8d460a 100644
--- a/doc/development/interacting_components.md
+++ b/doc/development/interacting_components.md
@@ -9,8 +9,8 @@ when making _backend_ changes that might involve multiple features or [component
## Uploads
-GitLab supports uploads to [object storage]. That means every feature and
-change that affects uploads should also be tested against [object storage],
+GitLab supports uploads to [object storage]. That means every feature and
+change that affects uploads should also be tested against [object storage],
which is _not_ enabled by default in [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit).
When working on a related feature, make sure to enable and test it
diff --git a/doc/development/new_fe_guide/development/testing.md b/doc/development/new_fe_guide/development/testing.md
index f7ea496d935..e0d413b748b 100644
--- a/doc/development/new_fe_guide/development/testing.md
+++ b/doc/development/new_fe_guide/development/testing.md
@@ -1,361 +1,6 @@
-# Overview of Frontend Testing
+---
+redirect_to: '../../testing_guide/frontend_testing.md'
+---
-Tests relevant for frontend development can be found at the following places:
+This document was moved to [another location](../../testing_guide/frontend_testing.md).
-- `spec/javascripts/` which are run by Karma (command: `yarn karma`) and contain
- - [frontend unit tests](#frontend-unit-tests)
- - [frontend component tests](#frontend-component-tests)
- - [frontend integration tests](#frontend-integration-tests)
-- `spec/frontend/` which are run by Jest (command: `yarn jest`) and contain
- - [frontend unit tests](#frontend-unit-tests)
- - [frontend component tests](#frontend-component-tests)
- - [frontend integration tests](#frontend-integration-tests)
-- `spec/features/` which are run by RSpec and contain
- - [feature tests](#feature-tests)
-
-All tests in `spec/javascripts/` will eventually be migrated to `spec/frontend/` (see also [#52483](https://gitlab.com/gitlab-org/gitlab-ce/issues/52483)).
-
-In addition there were feature tests in `features/` run by Spinach in the past.
-These have been removed from our codebase in May 2018 ([#23036](https://gitlab.com/gitlab-org/gitlab-ce/issues/23036)).
-
-See also:
-
-- [Old testing guide](../../testing_guide/frontend_testing.html).
-- [Notes on testing Vue components](../../fe_guide/vue.html#testing-vue-components).
-
-## Frontend unit tests
-
-Unit tests are on the lowest abstraction level and typically test functionality that is not directly perceivable by a user.
-
-### When to use unit tests
-
-<details>
- <summary>exported functions and classes</summary>
- Anything that is exported can be reused at various places in a way you have no control over.
- Therefore it is necessary to document the expected behavior of the public interface with tests.
-</details>
-
-<details>
- <summary>Vuex actions</summary>
- Any Vuex action needs to work in a consistent way independent of the component it is triggered from.
-</details>
-
-<details>
- <summary>Vuex mutations</summary>
- For complex Vuex mutations it helps to identify the source of a problem by separating the tests from other parts of the Vuex store.
-</details>
-
-### When *not* to use unit tests
-
-<details>
- <summary>non-exported functions or classes</summary>
- Anything that is not exported from a module can be considered private or an implementation detail and doesn't need to be tested.
-</details>
-
-<details>
- <summary>constants</summary>
- Testing the value of a constant would mean to copy it.
- This results in extra effort without additional confidence that the value is correct.
-</details>
-
-<details>
- <summary>Vue components</summary>
- Computed properties, methods, and lifecycle hooks can be considered an implementation detail of components and don't need to be tested.
- They are implicitly covered by component tests.
- The <a href="https://vue-test-utils.vuejs.org/guides/#getting-started">official Vue guidelines</a> suggest the same.
-</details>
-
-### What to mock in unit tests
-
-<details>
- <summary>state of the class under test</summary>
- Modifying the state of the class under test directly rather than using methods of the class avoids side-effects in test setup.
-</details>
-
-<details>
- <summary>other exported classes</summary>
- Every class needs to be tested in isolation to prevent test scenarios from growing exponentially.
-</details>
-
-<details>
- <summary>single DOM elements if passed as parameters</summary>
- For tests that only operate on single DOM elements rather than a whole page, creating these elements is cheaper than loading a whole HTML fixture.
-</details>
-
-<details>
- <summary>all server requests</summary>
- When running frontend unit tests, the backend may not be reachable.
- Therefore all outgoing requests need to be mocked.
-</details>
-
-<details>
- <summary>asynchronous background operations</summary>
- Background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects.
-</details>
-
-### What *not* to mock in unit tests
-
-<details>
- <summary>non-exported functions or classes</summary>
- Everything that is not exported can be considered private to the module and will be implicitly tested via the exported classes / functions.
-</details>
-
-<details>
- <summary>methods of the class under test</summary>
- By mocking methods of the class under test, the mocks will be tested and not the real methods.
-</details>
-
-<details>
- <summary>utility functions (pure functions, or those that only modify parameters)</summary>
- If a function has no side effects because it has no state, it is safe to not mock it in tests.
-</details>
-
-<details>
- <summary>full HTML pages</summary>
- Loading the HTML of a full page slows down tests, so it should be avoided in unit tests.
-</details>
-
-## Frontend component tests
-
-Component tests cover the state of a single component that is perceivable by a user depending on external signals such as user input, events fired from other components, or application state.
-
-### When to use component tests
-
-- Vue components
-
-### When *not* to use component tests
-
-<details>
- <summary>Vue applications</summary>
- Vue applications may contain many components.
- Testing them on a component level requires too much effort.
- Therefore they are tested on frontend integration level.
-</details>
-
-<details>
- <summary>HAML templates</summary>
- HAML templates contain only Markup and no frontend-side logic.
- Therefore they are not complete components.
-</details>
-
-### What to mock in component tests
-
-<details>
- <summary>DOM</summary>
- Operating on the real DOM is significantly slower than on the virtual DOM.
-</details>
-
-<details>
- <summary>properties and state of the component under test</summary>
- Similarly to testing classes, modifying the properties directly (rather than relying on methods of the component) avoids side-effects.
-</details>
-
-<details>
- <summary>Vuex store</summary>
- To avoid side effects and keep component tests simple, Vuex stores are replaced with mocks.
-</details>
-
-<details>
- <summary>all server requests</summary>
- Similar to unit tests, when running component tests, the backend may not be reachable.
- Therefore all outgoing requests need to be mocked.
-</details>
-
-<details>
- <summary>asynchronous background operations</summary>
- Similar to unit tests, background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects.
-</details>
-
-<details>
- <summary>child components</summary>
- Every component is tested individually, so child components are mocked.
- See also <a href="https://vue-test-utils.vuejs.org/api/#shallowmount">shallowMount()</a>
-</details>
-
-### What *not* to mock in component tests
-
-<details>
- <summary>methods or computed properties of the component under test</summary>
- By mocking part of the component under test, the mocks will be tested and not the real component.
-</details>
-
-<details>
- <summary>functions and classes independent from Vue</summary>
- All plain JavaScript code is already covered by unit tests and needs not to be mocked in component tests.
-</details>
-
-## Frontend integration tests
-
-Integration tests cover the interaction between all components on a single page.
-Their abstraction level is comparable to how a user would interact with the UI.
-
-### When to use integration tests
-
-<details>
- <summary>page bundles (<code>index.js</code> files in <code>app/assets/javascripts/pages/</code>)</summary>
- Testing the page bundles ensures the corresponding frontend components integrate well.
-</details>
-
-<details>
- <summary>Vue applications outside of page bundles</summary>
- Testing Vue applications as a whole ensures the corresponding frontend components integrate well.
-</details>
-
-### What to mock in integration tests
-
-<details>
- <summary>HAML views (use fixtures instead)</summary>
- Rendering HAML views requires a Rails environment including a running database which we cannot rely on in frontend tests.
-</details>
-
-<details>
- <summary>all server requests</summary>
- Similar to unit and component tests, when running component tests, the backend may not be reachable.
- Therefore all outgoing requests need to be mocked.
-</details>
-
-<details>
- <summary>asynchronous background operations that are not perceivable on the page</summary>
- Background operations that affect the page need to be tested on this level.
- All other background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects.
-</details>
-
-### What *not* to mock in integration tests
-
-<details>
- <summary>DOM</summary>
- Testing on the real DOM ensures our components work in the environment they are meant for.
- Part of this will be delegated to <a href="https://gitlab.com/gitlab-org/quality/team-tasks/issues/45">cross-browser testing</a>.
-</details>
-
-<details>
- <summary>properties or state of components</summary>
- On this level, all tests can only perform actions a user would do.
- For example to change the state of a component, a click event would be fired.
-</details>
-
-<details>
- <summary>Vuex stores</summary>
- When testing the frontend code of a page as a whole, the interaction between Vue components and Vuex stores is covered as well.
-</details>
-
-## Feature tests
-
-In contrast to [frontend integration tests](#frontend-integration-tests), feature tests make requests against the real backend instead of using fixtures.
-This also implies that database queries are executed which makes this category significantly slower.
-
-See also the [RSpec testing guidelines](../../testing_guide/best_practices.md#rspec).
-
-### When to use feature tests
-
-- use cases that require a backend and cannot be tested using fixtures
-- behavior that is not part of a page bundle but defined globally
-
-### Relevant notes
-
-A `:js` flag is added to the test to make sure the full environment is loaded.
-
-```
-scenario 'successfully', :js do
- sign_in(create(:admin))
-end
-```
-
-The steps of each test are written using capybara methods ([documentation](https://www.rubydoc.info/gems/capybara)).
-
-Bear in mind <abbr title="XMLHttpRequest">XHR</abbr> calls might require you to use `wait_for_requests` in between steps, like so:
-
-```rspec
-find('.form-control').native.send_keys(:enter)
-
-wait_for_requests
-
-expect(page).not_to have_selector('.card')
-```
-
-## Test helpers
-
-### Vuex Helper: `testAction`
-
-We have a helper available to make testing actions easier, as per [official documentation](https://vuex.vuejs.org/guide/testing.html):
-
-```
-testAction(
- actions.actionName, // action
- { }, // params to be passed to action
- state, // state
- [
- { type: types.MUTATION},
- { type: types.MUTATION_1, payload: {}},
- ], // mutations committed
- [
- { type: 'actionName', payload: {}},
- { type: 'actionName1', payload: {}},
- ] // actions dispatched
- done,
-);
-```
-
-Check an example in [spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.js](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/javascripts/ide/stores/actions_spec.js).
-
-### Vue Helper: `mountComponent`
-
-To make mounting a Vue component easier and more readable, we have a few helpers available in `spec/helpers/vue_mount_component_helper`.
-
-- `createComponentWithStore`
-- `mountComponentWithStore`
-
-Examples of usage:
-
-```
-beforeEach(() => {
- vm = createComponentWithStore(Component, store);
-
- vm.$store.state.currentBranchId = 'master';
-
- vm.$mount();
-},
-```
-
-```
-beforeEach(() => {
- vm = mountComponentWithStore(Component, {
- el: '#dummy-element',
- store,
- props: { badge },
- });
-},
-```
-
-Don't forget to clean up:
-
-```
-afterEach(() => {
- vm.$destroy();
-});
-```
-
-## Testing with older browsers
-
-Some regressions only affect a specific browser version. We can install and test in particular browsers with either Firefox or Browserstack using the following steps:
-
-### Browserstack
-
-[Browserstack](https://www.browserstack.com/) allows you to test more than 1200 mobile devices and browsers.
-You can use it directly through the [live app](https://www.browserstack.com/live) or you can install the [chrome extension](https://chrome.google.com/webstore/detail/browserstack/nkihdmlheodkdfojglpcjjmioefjahjb) for easy access.
-You can find the credentials on 1Password, under `frontendteam@gitlab.com`.
-
-### Firefox
-
-#### macOS
-
-You can download any older version of Firefox from the releases FTP server, <https://ftp.mozilla.org/pub/firefox/releases/>
-
-1. From the website, select a version, in this case `50.0.1`.
-1. Go to the mac folder.
-1. Select your preferred language, you will find the dmg package inside, download it.
-1. Drag and drop the application to any other folder but the `Applications` folder.
-1. Rename the application to something like `Firefox_Old`.
-1. Move the application to the `Applications` folder.
-1. Open up a terminal and run `/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager` to create a new profile specific to that Firefox version.
-1. Once the profile has been created, quit the app, and run it again like normal. You now have a working older Firefox version.
diff --git a/doc/development/new_fe_guide/modules/dirty_submit.md b/doc/development/new_fe_guide/modules/dirty_submit.md
index 6c03958b463..217743ea395 100644
--- a/doc/development/new_fe_guide/modules/dirty_submit.md
+++ b/doc/development/new_fe_guide/modules/dirty_submit.md
@@ -1,7 +1,6 @@
# Dirty Submit
-> [Introduced][ce-21115] in GitLab 11.3.
-> [dirty_submit][dirty-submit]
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21115) in GitLab 11.3.
## Summary
@@ -9,6 +8,9 @@ Prevent submitting forms with no changes.
Currently handles `input`, `textarea` and `select` elements.
+Also, see [the code](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/dirty_submit/)
+within the GitLab project.
+
## Usage
```js
@@ -18,6 +20,3 @@ new DirtySubmitForm(document.querySelector('form'));
// or
new DirtySubmitForm(document.querySelectorAll('form'));
```
-
-[ce-21115]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21115
-[dirty-submit]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/dirty_submit/ \ No newline at end of file
diff --git a/doc/development/new_fe_guide/style/javascript.md b/doc/development/new_fe_guide/style/javascript.md
index 802ebd12d92..b742d567f41 100644
--- a/doc/development/new_fe_guide/style/javascript.md
+++ b/doc/development/new_fe_guide/style/javascript.md
@@ -192,4 +192,4 @@ rules only if you are invoking/instantiating existing code modules.
- [class-method-use-this](http://eslint.org/docs/rules/class-methods-use-this)
> Note: Disable these rules on a per line basis. This makes it easier to refactor
- in the future. E.g. use `eslint-disable-next-line` or `eslint-disable-line`.
+> in the future. E.g. use `eslint-disable-next-line` or `eslint-disable-line`.
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
index a6b60149ea4..3787e2ef187 100644
--- a/doc/development/query_recorder.md
+++ b/doc/development/query_recorder.md
@@ -36,6 +36,13 @@ it "avoids N+1 database queries" do
end
```
+## Use request specs instead of controller specs
+
+Use a [request spec](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/requests) when writing a N+1 test on the controller level.
+
+Controller specs should not be used to write N+1 tests as the controller is only initialized once per example.
+This could lead to false successes where subsequent "requests" could have queries reduced (e.g. because of memoization).
+
## Finding the source of the query
It may be useful to identify the source of the queries by looking at the call backtrace.
diff --git a/doc/development/shell_scripting_guide/index.md b/doc/development/shell_scripting_guide/index.md
index ae7f2154682..0809f8b1a0a 100644
--- a/doc/development/shell_scripting_guide/index.md
+++ b/doc/development/shell_scripting_guide/index.md
@@ -24,7 +24,8 @@ Having said all of the above, we recommend staying away from shell scripts
as much as possible. A language like Ruby or Python (if required for
consistency with codebases that we leverage) is almost always a better choice.
The high-level interpreted languages have more readable syntax, offer much more
-mature capabilities for unit-testing, linting, and error reporting.
+mature capabilities for unit-testing, linting, and error reporting.
+
Use shell scripts only if there's a strong restriction on project's
dependencies size or any other requirements that are more important
in a particular case.
@@ -48,12 +49,12 @@ that is:
This section describes the tools that should be made a mandatory part of
a project's CI pipeline if it contains shell scripts. These tools
-automate shell code formatting, checking for errors or vulnerabilities, etc.
+automate shell code formatting, checking for errors or vulnerabilities, etc.
### Linting
We're using the [ShellCheck](https://www.shellcheck.net/) utility in its default configuration to lint our
-shell scripts.
+shell scripts.
All projects with shell scripts should use this GitLab CI/CD job:
@@ -98,7 +99,7 @@ NOTE: **Note:**
This is a work in progress.
It is an [ongoing effort](https://gitlab.com/gitlab-org/gitlab-ce/issues/64016) to evaluate different tools for the
-automated testing of shell scripts (like [BATS](https://github.com/sstephenson/bats)).
+automated testing of shell scripts (like [BATS](https://github.com/sstephenson/bats)).
## Code Review
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 2985278cc92..7dc89a3fcdb 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -232,7 +232,7 @@ module. GitLab has a custom `spyOnDependency` method which utilizes
[babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to
achieve this. It can be used like so:
-```js
+```javascript
// my_module.js
import { visitUrl } from '~/lib/utils/url_utility';
@@ -241,7 +241,7 @@ export default function doSomething() {
}
```
-```js
+```javascript
// my_module_spec.js
import doSomething from '~/my_module';
@@ -593,6 +593,517 @@ end
[capybara]: https://github.com/teamcapybara/capybara
[jasmine]: https://jasmine.github.io/
+## Overview of Frontend Testing Levels
+
+Tests relevant for frontend development can be found at the following places:
+
+- `spec/javascripts/` which are run by Karma (command: `yarn karma`) and contain
+ - [frontend unit tests](#frontend-unit-tests)
+ - [frontend component tests](#frontend-component-tests)
+ - [frontend integration tests](#frontend-integration-tests)
+- `spec/frontend/` which are run by Jest (command: `yarn jest`) and contain
+ - [frontend unit tests](#frontend-unit-tests)
+ - [frontend component tests](#frontend-component-tests)
+ - [frontend integration tests](#frontend-integration-tests)
+- `spec/features/` which are run by RSpec and contain
+ - [feature tests](#feature-tests)
+
+All tests in `spec/javascripts/` will eventually be migrated to `spec/frontend/` (see also [#52483](https://gitlab.com/gitlab-org/gitlab-ce/issues/52483)).
+
+In addition, there used to be feature tests in `features/`, run by Spinach.
+These were removed from the codebase in May 2018 ([#23036](https://gitlab.com/gitlab-org/gitlab-ce/issues/23036)).
+
+See also [Notes on testing Vue components](../fe_guide/vue.html#testing-vue-components).
+
+### Frontend unit tests
+
+Unit tests are on the lowest abstraction level and typically test functionality that is not directly perceivable by a user.
+
+```mermaid
+graph RL
+ plain[Plain JavaScript];
+ Vue[Vue Components];
+ feature-flags[Feature Flags];
+ license-checks[License Checks];
+
+ plain---Vuex;
+ plain---GraphQL;
+ Vue---plain;
+ Vue---Vuex;
+ Vue---GraphQL;
+ browser---plain;
+ browser---Vue;
+ plain---backend;
+ Vuex---backend;
+ GraphQL---backend;
+ Vue---backend;
+ backend---database;
+ backend---feature-flags;
+ backend---license-checks;
+
+ class plain tested;
+ class Vuex tested;
+
+ classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
+ classDef label stroke-width:0;
+ classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ subgraph " "
+ tested;
+ mocked;
+ class tested tested;
+ end
+```
+
+#### When to use unit tests
+
+<details>
+ <summary>exported functions and classes</summary>
+ Anything that is exported can be reused at various places in a way you have no control over.
+ Therefore it is necessary to document the expected behavior of the public interface with tests.
+</details>
+
+<details>
+ <summary>Vuex actions</summary>
+ Any Vuex action needs to work in a consistent way independent of the component it is triggered from.
+</details>
+
+<details>
+ <summary>Vuex mutations</summary>
+ For complex Vuex mutations it helps to identify the source of a problem by separating the tests from other parts of the Vuex store.
+</details>
+
+#### When *not* to use unit tests
+
+<details>
+ <summary>non-exported functions or classes</summary>
+ Anything that is not exported from a module can be considered private or an implementation detail and doesn't need to be tested.
+</details>
+
+<details>
+ <summary>constants</summary>
+ Testing the value of a constant would mean to copy it.
+ This results in extra effort without additional confidence that the value is correct.
+</details>
+
+<details>
+ <summary>Vue components</summary>
+ Computed properties, methods, and lifecycle hooks can be considered an implementation detail of components and don't need to be tested.
+ They are implicitly covered by component tests.
+ The <a href="https://vue-test-utils.vuejs.org/guides/#getting-started">official Vue guidelines</a> suggest the same.
+</details>
+
+#### What to mock in unit tests
+
+<details>
+ <summary>state of the class under test</summary>
+ Modifying the state of the class under test directly rather than using methods of the class avoids side-effects in test setup.
+</details>
+
+<details>
+ <summary>other exported classes</summary>
+ Every class needs to be tested in isolation to prevent test scenarios from growing exponentially.
+</details>
+
+<details>
+ <summary>single DOM elements if passed as parameters</summary>
+ For tests that only operate on single DOM elements rather than a whole page, creating these elements is cheaper than loading a whole HTML fixture.
+</details>
+
+<details>
+ <summary>all server requests</summary>
+ When running frontend unit tests, the backend may not be reachable.
+ Therefore all outgoing requests need to be mocked.
+</details>
+
+<details>
+ <summary>asynchronous background operations</summary>
+ Background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects.
+</details>
+
+#### What *not* to mock in unit tests
+
+<details>
+ <summary>non-exported functions or classes</summary>
+ Everything that is not exported can be considered private to the module and will be implicitly tested via the exported classes / functions.
+</details>
+
+<details>
+ <summary>methods of the class under test</summary>
+ By mocking methods of the class under test, the mocks will be tested and not the real methods.
+</details>
+
+<details>
+ <summary>utility functions (pure functions, or those that only modify parameters)</summary>
+ If a function has no side effects because it has no state, it is safe to not mock it in tests.
+</details>
+
+<details>
+ <summary>full HTML pages</summary>
+ Loading the HTML of a full page slows down tests, so it should be avoided in unit tests.
+</details>
+
+### Frontend component tests
+
+Component tests cover the state of a single component that is perceivable by a user depending on external signals such as user input, events fired from other components, or application state.
+
+```mermaid
+graph RL
+ plain[Plain JavaScript];
+ Vue[Vue Components];
+ feature-flags[Feature Flags];
+ license-checks[License Checks];
+
+ plain---Vuex;
+ plain---GraphQL;
+ Vue---plain;
+ Vue---Vuex;
+ Vue---GraphQL;
+ browser---plain;
+ browser---Vue;
+ plain---backend;
+ Vuex---backend;
+ GraphQL---backend;
+ Vue---backend;
+ backend---database;
+ backend---feature-flags;
+ backend---license-checks;
+
+ class Vue tested;
+
+ classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
+ classDef label stroke-width:0;
+ classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ subgraph " "
+ tested;
+ mocked;
+ class tested tested;
+ end
+```
+
+#### When to use component tests
+
+- Vue components
+
+#### When *not* to use component tests
+
+<details>
+ <summary>Vue applications</summary>
+ Vue applications may contain many components.
+ Testing them on a component level requires too much effort.
+ Therefore they are tested on frontend integration level.
+</details>
+
+<details>
+ <summary>HAML templates</summary>
+ HAML templates contain only Markup and no frontend-side logic.
+ Therefore they are not complete components.
+</details>
+
+#### What to mock in component tests
+
+<details>
+ <summary>DOM</summary>
+ Operating on the real DOM is significantly slower than on the virtual DOM.
+</details>
+
+<details>
+ <summary>properties and state of the component under test</summary>
+ Similarly to testing classes, modifying the properties directly (rather than relying on methods of the component) avoids side-effects.
+</details>
+
+<details>
+ <summary>Vuex store</summary>
+ To avoid side effects and keep component tests simple, Vuex stores are replaced with mocks.
+</details>
+
+<details>
+ <summary>all server requests</summary>
+ Similar to unit tests, when running component tests, the backend may not be reachable.
+ Therefore all outgoing requests need to be mocked.
+</details>
+
+<details>
+ <summary>asynchronous background operations</summary>
+ Similar to unit tests, background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects.
+</details>
+
+<details>
+ <summary>child components</summary>
+ Every component is tested individually, so child components are mocked.
+ See also <a href="https://vue-test-utils.vuejs.org/api/#shallowmount">shallowMount()</a>
+</details>
+
+#### What *not* to mock in component tests
+
+<details>
+ <summary>methods or computed properties of the component under test</summary>
+ By mocking part of the component under test, the mocks will be tested and not the real component.
+</details>
+
+<details>
+ <summary>functions and classes independent from Vue</summary>
+ All plain JavaScript code is already covered by unit tests and needs not to be mocked in component tests.
+</details>
+
+### Frontend integration tests
+
+Integration tests cover the interaction between all components on a single page.
+Their abstraction level is comparable to how a user would interact with the UI.
+
+```mermaid
+graph RL
+ plain[Plain JavaScript];
+ Vue[Vue Components];
+ feature-flags[Feature Flags];
+ license-checks[License Checks];
+
+ plain---Vuex;
+ plain---GraphQL;
+ Vue---plain;
+ Vue---Vuex;
+ Vue---GraphQL;
+ browser---plain;
+ browser---Vue;
+ plain---backend;
+ Vuex---backend;
+ GraphQL---backend;
+ Vue---backend;
+ backend---database;
+ backend---feature-flags;
+ backend---license-checks;
+
+ class plain tested;
+ class Vue tested;
+ class Vuex tested;
+ class GraphQL tested;
+ class browser tested;
+ linkStyle 0,1,2,3,4,5,6 stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
+ classDef label stroke-width:0;
+ classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ subgraph " "
+ tested;
+ mocked;
+ class tested tested;
+ end
+```
+
+#### When to use integration tests
+
+<details>
+ <summary>page bundles (<code>index.js</code> files in <code>app/assets/javascripts/pages/</code>)</summary>
+ Testing the page bundles ensures the corresponding frontend components integrate well.
+</details>
+
+<details>
+ <summary>Vue applications outside of page bundles</summary>
+ Testing Vue applications as a whole ensures the corresponding frontend components integrate well.
+</details>
+
+#### What to mock in integration tests
+
+<details>
+ <summary>HAML views (use fixtures instead)</summary>
+ Rendering HAML views requires a Rails environment including a running database which we cannot rely on in frontend tests.
+</details>
+
+<details>
+ <summary>all server requests</summary>
+ Similar to unit and component tests, when running component tests, the backend may not be reachable.
+ Therefore all outgoing requests need to be mocked.
+</details>
+
+<details>
+ <summary>asynchronous background operations that are not perceivable on the page</summary>
+ Background operations that affect the page need to be tested on this level.
+ All other background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects.
+</details>
+
+#### What *not* to mock in integration tests
+
+<details>
+ <summary>DOM</summary>
+ Testing on the real DOM ensures our components work in the environment they are meant for.
+ Part of this will be delegated to <a href="https://gitlab.com/gitlab-org/quality/team-tasks/issues/45">cross-browser testing</a>.
+</details>
+
+<details>
+ <summary>properties or state of components</summary>
+ On this level, all tests can only perform actions a user would do.
+ For example to change the state of a component, a click event would be fired.
+</details>
+
+<details>
+ <summary>Vuex stores</summary>
+ When testing the frontend code of a page as a whole, the interaction between Vue components and Vuex stores is covered as well.
+</details>
+
+### Feature tests
+
+In contrast to [frontend integration tests](#frontend-integration-tests), feature tests make requests against the real backend instead of using fixtures.
+This also implies that database queries are executed which makes this category significantly slower.
+
+See also the [RSpec testing guidelines](../testing_guide/best_practices.md#rspec).
+
+```mermaid
+graph RL
+ plain[Plain JavaScript];
+ Vue[Vue Components];
+ feature-flags[Feature Flags];
+ license-checks[License Checks];
+
+ plain---Vuex;
+ plain---GraphQL;
+ Vue---plain;
+ Vue---Vuex;
+ Vue---GraphQL;
+ browser---plain;
+ browser---Vue;
+ plain---backend;
+ Vuex---backend;
+ GraphQL---backend;
+ Vue---backend;
+ backend---database;
+ backend---feature-flags;
+ backend---license-checks;
+
+ class backend tested;
+ class plain tested;
+ class Vue tested;
+ class Vuex tested;
+ class GraphQL tested;
+ class browser tested;
+ linkStyle 0,1,2,3,4,5,6,7,8,9,10 stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090
+ classDef label stroke-width:0;
+ classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5;
+
+ subgraph " "
+ tested;
+ mocked;
+ class tested tested;
+ end
+```
+
+#### When to use feature tests
+
+- Use cases that require a backend and cannot be tested using fixtures.
+- Behavior that is not part of a page bundle but defined globally.
+
+#### Relevant notes
+
+A `:js` flag is added to the test to make sure the full environment is loaded.
+
+```ruby
+scenario 'successfully', :js do
+ sign_in(create(:admin))
+end
+```
+
+The steps of each test are written using capybara methods ([documentation](https://www.rubydoc.info/gems/capybara)).
+
+Bear in mind <abbr title="XMLHttpRequest">XHR</abbr> calls might require you to use `wait_for_requests` in between steps, like so:
+
+```ruby
+find('.form-control').native.send_keys(:enter)
+
+wait_for_requests
+
+expect(page).not_to have_selector('.card')
+```
+
+## Test helpers
+
+### Vuex Helper: `testAction`
+
+We have a helper available to make testing actions easier, as per [official documentation](https://vuex.vuejs.org/guide/testing.html):
+
+```javascript
+testAction(
+ actions.actionName, // action
+ { }, // params to be passed to action
+ state, // state
+ [
+ { type: types.MUTATION},
+ { type: types.MUTATION_1, payload: {}},
+ ], // mutations committed
+ [
+ { type: 'actionName', payload: {}},
+ { type: 'actionName1', payload: {}},
+ ] // actions dispatched
+ done,
+);
+```
+
+Check an example in [spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.js](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/javascripts/ide/stores/actions_spec.js).
+
+### Vue Helper: `mountComponent`
+
+To make mounting a Vue component easier and more readable, we have a few helpers available in `spec/helpers/vue_mount_component_helper`:
+
+- `createComponentWithStore`
+- `mountComponentWithStore`
+
+Examples of usage:
+
+```javascript
+beforeEach(() => {
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.currentBranchId = 'master';
+
+ vm.$mount();
+});
+```
+
+```javascript
+beforeEach(() => {
+ vm = mountComponentWithStore(Component, {
+ el: '#dummy-element',
+ store,
+ props: { badge },
+ });
+});
+```
+
+Don't forget to clean up:
+
+```javascript
+afterEach(() => {
+ vm.$destroy();
+});
+```
+
+## Testing with older browsers
+
+Some regressions only affect a specific browser version. We can install and test in particular browsers with either Firefox or Browserstack using the following steps:
+
+### Browserstack
+
+[Browserstack](https://www.browserstack.com/) allows you to test more than 1200 mobile devices and browsers.
+You can use it directly through the [live app](https://www.browserstack.com/live) or you can install the [chrome extension](https://chrome.google.com/webstore/detail/browserstack/nkihdmlheodkdfojglpcjjmioefjahjb) for easy access.
+You can find the credentials on 1Password, under `frontendteam@gitlab.com`.
+
+### Firefox
+
+#### macOS
+
+You can download any older version of Firefox from the releases FTP server, <https://ftp.mozilla.org/pub/firefox/releases/>:
+
+1. From the website, select a version, in this case `50.0.1`.
+1. Go to the mac folder.
+1. Select your preferred language, you will find the dmg package inside, download it.
+1. Drag and drop the application to any other folder but the `Applications` folder.
+1. Rename the application to something like `Firefox_Old`.
+1. Move the application to the `Applications` folder.
+1. Open up a terminal and run `/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager` to create a new profile specific to that Firefox version.
+1. Once the profile has been created, quit the app, and run it again like normal. You now have a working older Firefox version.
+
---
[Return to Testing documentation](index.md)
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 7843fc4c874..11449712a04 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -255,8 +255,8 @@ that a machine will hit the "too many mount points" problem in the future.
thousands of unused Docker images.**
> We have to start somewhere and improve later. Also, we're using the
- CNG-mirror project to store these Docker images so that we can just wipe out
- the registry at some point, and use a new fresh, empty one.
+ > CNG-mirror project to store these Docker images so that we can just wipe out
+ > the registry at some point, and use a new fresh, empty one.
**How do we secure this from abuse? Apps are open to the world so we need to
find a way to limit it to only us.**
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index 6c3a3171d39..0090c84cbf0 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -63,7 +63,7 @@ They're useful to test permissions, redirections, what view is rendered etc.
| Code path | Tests path | Testing engine | Notes |
| --------- | ---------- | -------------- | ----- |
-| `app/controllers/` | `spec/controllers/` | RSpec | |
+| `app/controllers/` | `spec/controllers/` | RSpec | For N+1 tests, use [request specs](../query_recorder.md#use-request-specs-instead-of-controller-specs) |
| `app/mailers/` | `spec/mailers/` | RSpec | |
| `lib/api/` | `spec/requests/api/` | RSpec | |
| `app/assets/javascripts/` | `spec/javascripts/`, `spec/frontend/` | Karma & Jest | More details in the [Frontend Testing guide](frontend_testing.md) section. |
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 3e3f96fb31f..5de173abbff 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -63,8 +63,8 @@ git config --global user.email
You'll need to do this only once, since you are using the `--global` option. It tells
Git to always use this information for anything you do on that system. If you want
-to override this with a different username or email address for specific projects,
-you can run the command without the `--global` option when you’re in that project.
+to override this with a different username or email address for specific projects or repositories,
+you can run the command without the `--global` option when you’re in that project, and that will default to `--local`. You can read more on how Git manages configurations in the [Git Config](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) documentation.
## Check your information
@@ -102,8 +102,7 @@ files to your local computer, automatically preserving the Git connection with t
remote repository.
You can either clone it via HTTPS or [SSH](../ssh/README.md). If you chose to clone
-it via HTTPS, you'll have to enter your credentials every time you pull and push.
-With SSH, you enter your credentials only once.
+it via HTTPS, you'll have to enter your credentials every time you pull and push. You can read more about credential storage in the [Git Credentials documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). With SSH, you enter your credentials only once.
You can find both paths (HTTPS and SSH) by navigating to your project's landing page
and clicking **Clone**. GitLab will prompt you with both paths, from which you can copy
@@ -152,13 +151,15 @@ to get the main branch code, or the branch name of the branch you are currently
in.
```bash
-git pull REMOTE <name-of-branch>
+git pull <REMOTE> <name-of-branch>
```
-When you first clone a repository, REMOTE is typically `origin`. This is where the
+When you clone a repository, `REMOTE` is typically `origin`. This is where the
repository was cloned from, and it indicates the SSH or HTTPS URL of the repository
on the remote server. `<name-of-branch>` is usually `master`, but it may be any existing
-branch.
+branch. You can create additional named remotes and branches as necessary.
+
+You can learn more on how Git manages remote repositories in the [Git Remote documentation](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes).
### View your remote repositories
@@ -168,6 +169,8 @@ To view your remote repositories, type:
git remote -v
```
+The `-v` flag stands for verbose.
+
### Add a remote repository
To add a link to a remote repository:
@@ -186,7 +189,7 @@ following (spaces won't be recognized in the branch name, so you will need to us
hyphen or underscore):
```bash
-git checkout -b <name-of-branch>>
+git checkout -b <name-of-branch>
```
### Work on an existing branch
@@ -238,7 +241,7 @@ git commit -m "COMMENT TO DESCRIBE THE INTENTION OF THE COMMIT"
```
NOTE: **Note:**
-The `.` character typically means _all_ in Git.
+The `.` character means _all file changes in the current directory and all subdirectories_.
### Send changes to GitLab.com
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
index 543a222bd25..c5939dc6856 100644
--- a/doc/install/azure/index.md
+++ b/doc/install/azure/index.md
@@ -407,7 +407,7 @@ on any cloud service you choose.
## Where to next?
-Check out our other [Technical Articles](../../articles/index.md) or browse the [GitLab Documentation][GitLab-Docs](../../README.md) to learn more about GitLab.
+Check out our other [Technical Articles](../../articles/index.md) or browse the [GitLab Documentation](../../README.md) to learn more about GitLab.
### Useful links
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 9c1258fa1aa..b8ad552accc 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -584,6 +584,55 @@ Unless you have a `Dockerfile` in your repo, your image is built with
Herokuish, and you must prefix commands run in these images with `/bin/herokuish
procfile exec` to replicate the environment where your application will run.
+#### Workers
+
+Some web applications need to run extra deployments for "worker processes". For
+example, it is common in a Rails application to have a separate worker process
+to run background tasks like sending emails.
+
+The [default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app)
+used in Auto Deploy [has support for running worker
+processes](https://gitlab.com/gitlab-org/charts/auto-deploy-app/merge_requests/9).
+
+In order to run a worker, you'll need to ensure that it is able to respond to
+the standard health checks, which expect a successful HTTP response on port
+`5000`. For [Sidekiq](https://github.com/mperham/sidekiq), you could make use of
+the [`sidekiq_alive` gem](https://rubygems.org/gems/sidekiq_alive) to do this.
+
+In order to work with Sidekiq, you'll also need to ensure your deployments have
+access to a Redis instance. Auto DevOps won't deploy this for you so you'll
+need to:
+
+- Maintain your own Redis instance.
+- Set a CI variable `K8S_SECRET_REDIS_URL`, which the URL of this instance to
+ ensure it's passed into your deployments.
+
+Once you have configured your worker to respond to health checks, you will
+need to configure a CI variable `HELM_UPGRADE_EXTRA_ARGS` with the value
+`--values helm-values.yaml`.
+
+Then you can, for example, run a Sidekiq worker for your Rails application
+by adding a file named `helm-values.yaml` to your repository with the following
+content:
+
+```yml
+workers:
+ sidekiq:
+ replicaCount: 1
+ command:
+ - /bin/herokuish
+ - procfile
+ - exec
+ - sidekiq
+ preStopCommand:
+ - /bin/herokuish
+ - procfile
+ - exec
+ - sidekiqctl
+ - quiet
+ terminationGracePeriodSeconds: 60
+```
+
### Auto Monitoring
See the [requirements](#requirements) for Auto Monitoring to enable this stage.
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index e3f657af564..bd50367681e 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -39,6 +39,25 @@ However, users will not be prompted to log via SSO on each visit. GitLab will ch
We intend to add a similar SSO requirement for [Git and API activity](https://gitlab.com/gitlab-org/gitlab-ee/issues/9152) in the future.
+#### Group-managed accounts
+
+[Introduced in GitLab 12.1](https://gitlab.com/groups/gitlab-org/-/epics/709).
+
+When SSO is being enforced, groups can enable an additional level of protection by enforcing the creation of dedicated user accounts to access the group.
+
+Without group-managed accounts, users can link their SAML identity with any existing user on the instance. With group-managed accounts enabled, users are required to create a new, dedicated user linked to the group. The notification email address associated with the user is locked to the email address received from the configured identity provider.
+
+When this option is enabled:
+
+- All existing and new users in the group will be required to log in via the SSO URL associated with the group.
+- On successfully authenticating, GitLab will prompt the user to create a new, dedicated account using the email address received from the configured identity provider.
+- After the group managed account has been created, group activity will require the use of this user account.
+
+Since use of the group managed account requires the use of SSO, users of group managed accounts will lose access to these accounts when they are no longer able to authenticate with the connected identity provider. In the case of an offboarded employee who has been removed from your identity provider:
+
+- The user will be unable to access the group (their credentials will no longer work on the identity provider when prompted to SSO).
+- Contributions in the group (e.g. issues, merge requests) will remain intact.
+
### NameID
GitLab.com uses the SAML NameID to identify users. The NameID element:
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index e2b1a20a605..f7ba921aa7d 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -48,6 +48,7 @@ To enable 2FA:
- [andOTP](https://github.com/andOTP/andOTP): feature rich open source app for Android which supports PGP encrypted backups.
- [FreeOTP](https://freeotp.github.io/): open source app for Android.
- [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en): proprietary app for iOS and Android.
+ - [SailOTP](https://openrepos.net/content/seiichiro0185/sailotp): open source app for SailFish OS.
1. In the application, add a new entry in one of two ways:
- Scan the code presented in GitLab with your device's camera to add the
entry automatically.
diff --git a/doc/user/project/import/gitlab_com.md b/doc/user/project/import/gitlab_com.md
index f48a158e2d3..2f87f257754 100644
--- a/doc/user/project/import/gitlab_com.md
+++ b/doc/user/project/import/gitlab_com.md
@@ -1,21 +1,21 @@
# Project importing from GitLab.com to your private GitLab instance
-You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if
-GitLab.com integration is enabled on your GitLab instance.
+You can import your existing GitLab.com projects to your GitLab instance, but keep in
+mind that it is possible only if GitLab.com integration is enabled on your GitLab instance.
[Read more about GitLab.com integration for self-managed GitLab instances](../../../integration/gitlab.md).
To get to the importer page you need to go to "New project" page.
>**Note:**
-If you are interested in importing Wiki and Merge Request data to your new
-instance, you'll need to follow the instructions for [project export](../settings/import_export.md)
+If you are interested in importing Wiki and Merge Request data to your new instance,
+you'll need to follow the instructions for [exporting a project](../settings/import_export.md#exporting-a-project-and-its-data)
-![New project page](img/gitlab_new_project_page.png)
+![New project page](img/gitlab_new_project_page_v12_2.png)
-Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com
+Go to the **Import Projects** tab, then click on **GitLab.com**, and you will be redirected to GitLab.com
for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
![Importer page](img/gitlab_importer.png)
-To import a project, you can simple click "Import". The importer will import your repository and issues.
+To import a project, click "Import". The importer will import your repository and issues.
Once the importer is done, a new GitLab project will be created with your imported data.
diff --git a/doc/user/project/import/img/gitlab_new_project_page.png b/doc/user/project/import/img/gitlab_new_project_page.png
deleted file mode 100644
index c673724f436..00000000000
--- a/doc/user/project/import/img/gitlab_new_project_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/import/img/gitlab_new_project_page_v12_2.png b/doc/user/project/import/img/gitlab_new_project_page_v12_2.png
new file mode 100644
index 00000000000..e79c27f32c0
--- /dev/null
+++ b/doc/user/project/import/img/gitlab_new_project_page_v12_2.png
Binary files differ
diff --git a/doc/user/project/integrations/img/download_as_csv.png b/doc/user/project/integrations/img/download_as_csv.png
new file mode 100644
index 00000000000..0ed5ab8db89
--- /dev/null
+++ b/doc/user/project/integrations/img/download_as_csv.png
Binary files differ
diff --git a/doc/user/project/integrations/img/generate_link_to_chart.png b/doc/user/project/integrations/img/generate_link_to_chart.png
new file mode 100644
index 00000000000..03e018969b1
--- /dev/null
+++ b/doc/user/project/integrations/img/generate_link_to_chart.png
Binary files differ
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index aa7db97c413..e2f45b8a32f 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -269,6 +269,12 @@ Note the following properties:
![single stat panel type](img/prometheus_dashboard_single_stat_panel_type.png)
+### Downloading data as CSV
+
+Data from Prometheus charts on the metrics dashboard can be downloaded as CSV.
+
+![Downloading as CSV](img/download_as_csv.png)
+
### Setting up alerts for Prometheus metrics **(ULTIMATE)**
#### Managed Prometheus instances
@@ -363,6 +369,10 @@ It is possible to display metrics charts within [GitLab Flavored Markdown](../..
To display a metric chart, include a link of the form `https://<root_url>/<project>/environments/<environment_id>/metrics`.
+A single chart may also be embedded. You can generate a link to the chart via the dropdown located on the right side of the chart:
+
+![Generate Link To Chart](img/generate_link_to_chart.png)
+
The following requirements must be met for the metric to unfurl:
- The `<environment_id>` must correspond to a real environment.
diff --git a/doc/user/project/members/img/access_requests_management.png b/doc/user/project/members/img/access_requests_management.png
index 8996d9564d7..9a1c9621e41 100644
--- a/doc/user/project/members/img/access_requests_management.png
+++ b/doc/user/project/members/img/access_requests_management.png
Binary files differ
diff --git a/doc/user/project/members/img/request_access_button.png b/doc/user/project/members/img/request_access_button.png
index e8b490b91b8..e693f9a9ac2 100644
--- a/doc/user/project/members/img/request_access_button.png
+++ b/doc/user/project/members/img/request_access_button.png
Binary files differ
diff --git a/doc/user/project/members/img/withdraw_access_request_button.png b/doc/user/project/members/img/withdraw_access_request_button.png
index 6a3172dfcdb..e5a8fe0b356 100644
--- a/doc/user/project/members/img/withdraw_access_request_button.png
+++ b/doc/user/project/members/img/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/user/project/merge_requests/fast_forward_merge.md b/doc/user/project/merge_requests/fast_forward_merge.md
index 4f3c68090e4..2d59e535ce1 100644
--- a/doc/user/project/merge_requests/fast_forward_merge.md
+++ b/doc/user/project/merge_requests/fast_forward_merge.md
@@ -15,7 +15,7 @@ to accept merge requests without creating merge commits.
When the fast-forward merge
([`--ff-only`](https://git-scm.com/docs/git-merge#git-merge---ff-only)) setting
is enabled, no merge commits will be created and all merges are fast-forwarded,
-which means that merging is only allowed if the branch could be fast-forwarded.
+which means that merging is only allowed if the branch can be fast-forwarded.
When a fast-forward merge is not possible, the user is given the option to rebase.
@@ -28,9 +28,15 @@ When a fast-forward merge is not possible, the user is given the option to rebas
Now, when you visit the merge request page, you will be able to accept it
**only if a fast-forward merge is possible**.
+![Fast forward merge request](img/ff_merge_mr.png)
+
+If a fast-forward merge is not possible but a conflict free rebase is possible,
+a rebase button will be offered.
+
![Fast forward merge request](img/ff_merge_rebase.png)
-If the target branch is ahead of the source branch, you need to rebase the
+If the target branch is ahead of the source branch and a conflict free rebase is
+not possible, you need to rebase the
source branch locally before you will be able to do a fast-forward merge.
![Fast forward merge rebase locally](img/ff_merge_rebase_locally.png)
diff --git a/doc/user/project/merge_requests/img/ff_merge_mr.png b/doc/user/project/merge_requests/img/ff_merge_mr.png
new file mode 100644
index 00000000000..241cc990343
--- /dev/null
+++ b/doc/user/project/merge_requests/img/ff_merge_mr.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/incrementally_expand_merge_request_diffs_v12_2.png b/doc/user/project/merge_requests/img/incrementally_expand_merge_request_diffs_v12_2.png
new file mode 100644
index 00000000000..ee94dbdea5c
--- /dev/null
+++ b/doc/user/project/merge_requests/img/incrementally_expand_merge_request_diffs_v12_2.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 8a82b163481..2794cbc0f39 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -497,6 +497,15 @@ list.
![Merge request diff file navigation](img/merge_request_diff_file_navigation.png)
+### Incrementally expand merge request diffs
+
+By default, the diff shows only the parts of a file which are changed.
+To view more unchanged lines above or below a change click on the
+**Expand up** or **Expand down** icons. You can also click on **Show all lines**
+to expand the entire file.
+
+![Incrementally expand merge request diffs](img/incrementally_expand_merge_request_diffs_v12_2.png)
+
## Ignore whitespace changes in Merge Request diff view
If you click the **Hide whitespace changes** button, you can see the diff
diff --git a/doc/user/project/repository/img/repository_languages.png b/doc/user/project/repository/img/repository_languages.png
deleted file mode 100644
index 5977ad7faae..00000000000
--- a/doc/user/project/repository/img/repository_languages.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/repository/img/repository_languages_v12_2.gif b/doc/user/project/repository/img/repository_languages_v12_2.gif
new file mode 100644
index 00000000000..d0a0e57c919
--- /dev/null
+++ b/doc/user/project/repository/img/repository_languages_v12_2.gif
Binary files differ
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 84d63d29929..bd966185c94 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -182,7 +182,7 @@ were used and display this on the projects pages. If this information is missing
be added after updating the default branch on the project. This process can take up to 5
minutes.
-![Repository Languages bar](img/repository_languages.png)
+![Repository Languages bar](img/repository_languages_v12_2.gif)
Not all files are detected, among others; documentation,
vendored code, and most markup languages are excluded. This behaviour can be
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index e3c6cd6d6ff..d1bab47de25 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -28,11 +28,12 @@ NOTE: **Note:**
Requires Developer [permissions](../../permissions.md).
Create a new page by clicking the **New page** button that can be found
-in all wiki pages. You will be asked to fill in the page name from which GitLab
-will create the path to the page. You can specify a full path for the new file
-and any missing directories will be created automatically.
+in all wiki pages.
-![New page modal](img/wiki_create_new_page_modal.png)
+You will be asked to fill in a title for your new wiki page. Wiki titles
+also determine the path to the wiki page. You can specify a full path
+(using "`/`" for subdirectories) for the new title and any missing
+directories will be created automatically.
Once you enter the page name, it's time to fill in its content. GitLab wikis
support Markdown, RDoc and AsciiDoc. For Markdown based pages, all the
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 3160b0c4deb..7e880b3009d 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -46,8 +46,7 @@ In `config/gitlab.yml`:
## Storing LFS objects in remote object storage
-> [Introduced][ee-2760] in [GitLab Premium][eep] 10.0. Brought to GitLab Core
-in 10.7.
+> [Introduced][ee-2760] in [GitLab Premium][eep] 10.0. Brought to GitLab Core in 10.7.
It is possible to store LFS objects in remote object storage which allows you
to offload local hard disk R/W operations, and free up disk space significantly.
@@ -91,7 +90,7 @@ Here is a configuration example with S3.
| `aws_access_key_id` | AWS credentials, or compatible | `ABC123DEF456` |
| `aws_secret_access_key` | AWS credentials, or compatible | `ABC123DEF456ABC123DEF456ABC123DEF456` |
| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
-| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with AWS v4 signatures (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true
+| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
@@ -107,7 +106,9 @@ Here is a configuration example with GCS.
| `google_client_email` | The email address of the service account | `foo@gcp-project-12345.iam.gserviceaccount.com` |
| `google_json_key_location` | The json key path | `/path/to/gcp-project-12345-abcde.json` |
-_NOTE: The service account must have permission to access the bucket. [See more](https://cloud.google.com/storage/docs/authentication)_
+NOTE: **Note:**
+The service account must have permission to access the bucket.
+[See more](https://cloud.google.com/storage/docs/authentication)
Here is a configuration example with Rackspace Cloud Files.
@@ -119,7 +120,13 @@ Here is a configuration example with Rackspace Cloud Files.
| `rackspace_region` | The Rackspace storage region to use, a three letter code from the [list of service access endpoints](https://developer.rackspace.com/docs/cloud-files/v1/general-api-info/service-access/) | `iad` |
| `rackspace_temp_url_key` | The private key you have set in the Rackspace API for temporary URLs. Read more [here](https://developer.rackspace.com/docs/cloud-files/v1/use-cases/public-access-to-your-cloud-files-account/#tempurl) | `ABC123DEF456ABC123DEF456ABC123DE` |
-_NOTES: Regardless of whether the container has public access enabled or disabled, Fog will use the TempURL method to grant access to LFS objects. If you see errors in logs referencing instantiating storage with a temp-url-key, ensure that you have set they key properly on the Rackspace API and in gitlab.rb. You can verify the value of the key Rackspace has set by sending a GET request with token header to the service access endpoint URL and comparing the output of the returned headers._
+NOTE: **Note:**
+Regardless of whether the container has public access enabled or disabled, Fog will
+use the TempURL method to grant access to LFS objects. If you see errors in logs referencing
+instantiating storage with a temp-url-key, ensure that you have set they key properly
+on the Rackspace API and in gitlab.rb. You can verify the value of the key Rackspace
+has set by sending a GET request with token header to the service access endpoint URL
+and comparing the output of the returned headers.
### Manual uploading to an object storage
@@ -167,13 +174,13 @@ On Omnibus installations, the settings are prefixed by `lfs_object_store_`:
1. Save the file and [reconfigure GitLab]s for the changes to take effect.
1. Migrate any existing local LFS objects to the object storage:
- ```bash
- gitlab-rake gitlab:lfs:migrate
- ```
+ ```bash
+ gitlab-rake gitlab:lfs:migrate
+ ```
- This will migrate existing LFS objects to object storage. New LFS objects
- will be forwarded to object storage unless
- `gitlab_rails['lfs_object_store_background_upload']` is set to false.
+ This will migrate existing LFS objects to object storage. New LFS objects
+ will be forwarded to object storage unless
+ `gitlab_rails['lfs_object_store_background_upload']` is set to false.
### S3 for installations from source
@@ -203,13 +210,13 @@ For source installations the settings are nested under `lfs:` and then
1. Save the file and [restart GitLab][] for the changes to take effect.
1. Migrate any existing local LFS objects to the object storage:
- ```bash
- sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production
- ```
+ ```bash
+ sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production
+ ```
- This will migrate existing LFS objects to object storage. New LFS objects
- will be forwarded to object storage unless `background_upload` is set to
- false.
+ This will migrate existing LFS objects to object storage. New LFS objects
+ will be forwarded to object storage unless `background_upload` is set to
+ false.
## Storage statistics
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 264372a512d..f5593927cc2 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -35,9 +35,10 @@ Documentation for GitLab instance administrators is under [LFS administration do
- Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
to add the URL to Git config manually (see [troubleshooting](#troubleshooting))
->**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
- still goes over HTTP, but now the SSH client passes the correct credentials
- to the Git LFS client, so no action is required by the user.
+NOTE: **Note:**
+With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+still goes over HTTP, but now the SSH client passes the correct credentials
+to the Git LFS client, so no action is required by the user.
## Using Git LFS
@@ -61,12 +62,13 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server
```
-> **Note**: Make sure that `.gitattributes` is tracked by git. Otherwise Git
-> LFS will not be working properly for people cloning the project.
->
-> ```bash
-> git add .gitattributes
-> ```
+NOTE: **Note:**
+**Make sure** that `.gitattributes` is tracked by Git. Otherwise Git
+LFS will not be working properly for people cloning the project.
+
+```bash
+git add .gitattributes
+```
Cloning the repository works the same as before. Git automatically detects the
LFS-tracked files and clones them via HTTP. If you performed the git clone
@@ -216,9 +218,10 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
### Credentials are always required when pushing an object
->**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
- still goes over HTTP, but now the SSH client passes the correct credentials
- to the Git LFS client, so no action is required by the user.
+NOTE: **Note:**
+With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+still goes over HTTP, but now the SSH client passes the correct credentials
+to the Git LFS client, so no action is required by the user.
Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing
the LFS object on every push for every object, user HTTPS credentials are required.
diff --git a/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
index 75673d23799..5ceeb3253f1 100644
--- a/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
+++ b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md
@@ -76,7 +76,7 @@ Here you'll find a guide on
Since Annex files are stored as objects with symlinks and cannot be directly
modified, we need to first remove those symlinks.
->**Note:**
+NOTE: **Note:**
Make sure the you read about the [`direct` mode][annex-direct] as it contains
useful information that may fit in your use case. Note that `annex direct` is
deprecated in Git Annex version 6, so you may need to upgrade your repository
diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md
index 0b8e7d851b0..753518d0424 100644
--- a/doc/workflow/repository_mirroring.md
+++ b/doc/workflow/repository_mirroring.md
@@ -37,8 +37,7 @@ The following are some possible use cases for repository mirroring:
## Pushing to a remote repository **(CORE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise
-> Edition 8.7. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise Edition 8.7. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
For an existing project, you can set up push mirroring as follows:
@@ -67,8 +66,7 @@ section.
### Push only protected branches **(CORE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350) in
-> [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8.
You can choose to only push your protected branches from GitLab to your remote repository.
@@ -98,8 +96,7 @@ The repository will push soon. To force a push, click the appropriate button.
## Pulling from a remote repository **(STARTER)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2.
-> [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab-ee/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2. [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab-ee/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11.
NOTE: **Note:** This feature [is available for free](https://gitlab.com/gitlab-org/gitlab-ee/issues/10361) to
GitLab.com users until September 22nd, 2019.
@@ -157,8 +154,7 @@ Repository mirrors are updated as Sidekiq becomes available to process them. If
### SSH authentication
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5.
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5. [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6
SSH authentication is mutual:
@@ -245,8 +241,7 @@ key to keep the mirror running.
### Overwrite diverged branches **(STARTER)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4559) in
-> [GitLab Starter](https://about.gitlab.com/pricing/) 10.6.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4559) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.6.
You can choose to always update your local branches with remote versions, even if they have
diverged from the remote.
@@ -258,8 +253,7 @@ To use this option, check the **Overwrite diverged branches** box when creating
### Only mirror protected branches **(STARTER)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3326) in
-> [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3326) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
You can choose to pull mirror only the protected branches from your remote repository to GitLab.
Non-protected branches are not mirrored and can diverge.
@@ -268,8 +262,7 @@ To use this option, check the **Only mirror protected branches** box when creati
### Hard failure **(STARTER)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3117) in
-> [GitLab Starter](https://about.gitlab.com/pricing/) 10.2.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3117) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.2.
Once the mirroring process is unsuccessfully retried 14 times in a row, it will get marked as hard
failed. This will become visible in either the:
@@ -282,8 +275,7 @@ project mirroring again by [Forcing an update](#forcing-an-update-core).
### Trigger update using API **(STARTER)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3453) in
-[GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3453) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3.
Pull mirroring uses polling to detect new branches and commits added upstream, often minutes
afterwards. If you notify GitLab by [API](../api/projects.md#start-the-pull-mirroring-process-for-a-project-starter),
@@ -325,9 +317,10 @@ protected branches.
### Preventing conflicts using a `pre-receive` hook
-> **Warning:** The solution proposed will negatively impact the performance of
-> Git push operations because they will be proxied to the upstream Git
-> repository.
+CAUTION: **Warning:**
+The solution proposed will negatively impact the performance of
+Git push operations because they will be proxied to the upstream Git
+repository.
A server-side `pre-receive` hook can be used to prevent the race condition
described above by only accepting the push after first pushing the commit to
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 0432c406f31..211d242e2d3 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -35,8 +35,8 @@ A To Do displays on your To-Do List when:
- You are `@mentioned` in a comment on a commit
- A job in the CI pipeline running for your merge request failed, but this
job is not allowed to fail
-- An open merge request becomes unmergeable due to conflict, and you are either:
- - The author
+- An open merge request becomes unmergeable due to conflict, and you are either:
+ - The author
- Have set it to automatically merge once the pipeline succeeds
To-do triggers are not affected by [GitLab Notification Email settings](notifications.md).
diff --git a/lib/api/api.rb b/lib/api/api.rb
index e500a93b31e..219ed45eff6 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -18,7 +18,7 @@ module API
formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
include: [
GrapeLogging::Loggers::FilterParameters.new(LOG_FILTERS),
- GrapeLogging::Loggers::ClientEnv.new,
+ Gitlab::GrapeLogging::Loggers::ClientEnvLogger.new,
Gitlab::GrapeLogging::Loggers::RouteLogger.new,
Gitlab::GrapeLogging::Loggers::UserLogger.new,
Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new,
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index a1851ba3627..89b7e5c5e4b 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -69,12 +69,12 @@ module API
post endpoint do
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
- award = awardable.create_award_emoji(params[:name], current_user)
+ service = AwardEmojis::AddService.new(awardable, params[:name], current_user).execute
- if award.persisted?
- present award, with: Entities::AwardEmoji
+ if service[:status] == :success
+ present service[:award], with: Entities::AwardEmoji
else
- not_found!("Award Emoji #{award.errors.messages}")
+ not_found!("Award Emoji #{service[:message]}")
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 09253ab6b0e..5e66b4e76a5 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -645,7 +645,10 @@ module API
end
end
- expose :subscribed do |issue, options|
+ # Calculating the value of subscribed field triggers Markdown
+ # processing. We can't do that for multiple issues / merge
+ # requests in a single API request.
+ expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |issue, options|
issue.subscribed?(options[:current_user], options[:project] || issue.project)
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index d687acf3423..7819c2de515 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -96,7 +96,8 @@ module API
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
- issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
+ issuable_metadata: issuable_meta_data(issues, 'Issue', current_user),
+ include_subscribed: false
}
present issues, options
@@ -122,7 +123,8 @@ module API
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
- issuable_metadata: issuable_meta_data(issues, 'Issue', current_user)
+ issuable_metadata: issuable_meta_data(issues, 'Issue', current_user),
+ include_subscribed: false
}
present issues, options
diff --git a/lib/gitlab/action_rate_limiter.rb b/lib/gitlab/action_rate_limiter.rb
index fdb06d00548..0e8707af631 100644
--- a/lib/gitlab/action_rate_limiter.rb
+++ b/lib/gitlab/action_rate_limiter.rb
@@ -49,9 +49,9 @@ module Gitlab
request_information = {
message: 'Action_Rate_Limiter_Request',
env: type,
- ip: request.ip,
+ remote_ip: request.ip,
request_method: request.request_method,
- fullpath: request.fullpath
+ path: request.fullpath
}
if current_user
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
index 0698136166a..e9c8864123f 100644
--- a/lib/gitlab/ci/build/policy/variables.rb
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def satisfied_by?(pipeline, seed)
- variables = seed.to_resource.scoped_variables_hash
+ variables = seed.scoped_variables_hash
statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
new file mode 100644
index 00000000000..89623a809c9
--- /dev/null
+++ b/lib/gitlab/ci/build/rules.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules
+ include ::Gitlab::Utils::StrongMemoize
+
+ Result = Struct.new(:when, :start_in)
+
+ def initialize(rule_hashes, default_when = 'on_success')
+ @rule_list = Rule.fabricate_list(rule_hashes)
+ @default_when = default_when
+ end
+
+ def evaluate(pipeline, build)
+ if @rule_list.nil?
+ Result.new(@default_when)
+ elsif matched_rule = match_rule(pipeline, build)
+ Result.new(
+ matched_rule.attributes[:when] || @default_when,
+ matched_rule.attributes[:start_in]
+ )
+ else
+ Result.new('never')
+ end
+ end
+
+ private
+
+ def match_rule(pipeline, build)
+ @rule_list.find { |rule| rule.matches?(pipeline, build) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule.rb b/lib/gitlab/ci/build/rules/rule.rb
new file mode 100644
index 00000000000..8d52158c8d2
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule
+ attr_accessor :attributes
+
+ def self.fabricate_list(list)
+ list.map(&method(:new)) if list
+ end
+
+ def initialize(spec)
+ @clauses = []
+ @attributes = {}
+
+ spec.each do |type, value|
+ if clause = Clause.fabricate(type, value)
+ @clauses << clause
+ else
+ @attributes.merge!(type => value)
+ end
+ end
+ end
+
+ def matches?(pipeline, build)
+ @clauses.all? { |clause| clause.satisfied_by?(pipeline, build) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb
new file mode 100644
index 00000000000..ff0baf3348c
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause
+ ##
+ # Abstract class that defines an interface of a single
+ # job rule specification.
+ #
+ # Used for job's inclusion rules configuration.
+ #
+ UnknownClauseError = Class.new(StandardError)
+
+ def self.fabricate(type, value)
+ type = type.to_s.camelize
+
+ self.const_get(type).new(value) if self.const_defined?(type)
+ end
+
+ def initialize(spec)
+ @spec = spec
+ end
+
+ def satisfied_by?(pipeline, seed = nil)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
new file mode 100644
index 00000000000..81d2ee6c24c
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause::Changes < Rules::Rule::Clause
+ def initialize(globs)
+ @globs = Array(globs)
+ end
+
+ def satisfied_by?(pipeline, seed)
+ return true if pipeline.modified_paths.nil?
+
+ pipeline.modified_paths.any? do |path|
+ @globs.any? do |glob|
+ File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/if.rb b/lib/gitlab/ci/build/rules/rule/clause/if.rb
new file mode 100644
index 00000000000..18c3b450f95
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause/if.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause::If < Rules::Rule::Clause
+ def initialize(expression)
+ @expression = expression
+ end
+
+ def satisfied_by?(pipeline, seed)
+ variables = seed.scoped_variables_hash
+
+ ::Gitlab::Ci::Pipeline::Expression::Statement.new(@expression, variables).truthful?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 29a52b9da17..6e11c582750 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -11,7 +11,8 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[tags script only except type image services
+ ALLOWED_WHEN = %w[on_success on_failure always manual delayed].freeze
+ ALLOWED_KEYS = %i[tags script only except rules type image services
allow_failure type stage when start_in artifacts cache
dependencies needs before_script after_script variables
environment coverage retry parallel extends].freeze
@@ -19,12 +20,19 @@ module Gitlab
REQUIRED_BY_NEEDS = %i[stage].freeze
validations do
+ validates :config, type: Hash
validates :config, allowed_keys: ALLOWED_KEYS
validates :config, required_keys: REQUIRED_BY_NEEDS, if: :has_needs?
validates :config, presence: true
validates :script, presence: true
validates :name, presence: true
validates :name, type: Symbol
+ validates :config,
+ disallowed_keys: {
+ in: %i[only except when start_in],
+ message: 'key may not be used with `rules`'
+ },
+ if: :has_rules?
with_options allow_nil: true do
validates :tags, array_of_strings: true
@@ -32,17 +40,19 @@ module Gitlab
validates :parallel, numericality: { only_integer: true,
greater_than_or_equal_to: 2,
less_than_or_equal_to: 50 }
- validates :when,
- inclusion: { in: %w[on_success on_failure always manual delayed],
- message: 'should be on_success, on_failure, ' \
- 'always, manual or delayed' }
+ validates :when, inclusion: {
+ in: ALLOWED_WHEN,
+ message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
+ }
+
validates :dependencies, array_of_strings: true
validates :needs, array_of_strings: true
validates :extends, array_of_strings_or_string: true
+ validates :rules, array_of_hashes: true
end
validates :start_in, duration: { limit: '1 day' }, if: :delayed?
- validates :start_in, absence: true, unless: :delayed?
+ validates :start_in, absence: true, if: -> { has_rules? || !delayed? }
validate do
next unless dependencies.present?
@@ -91,6 +101,9 @@ module Gitlab
entry :except, Entry::Policy,
description: 'Refs policy this job will be executed for.'
+ entry :rules, Entry::Rules,
+ description: 'List of evaluable Rules to determine job inclusion.'
+
entry :variables, Entry::Variables,
description: 'Environment variables available for this job.'
@@ -112,7 +125,7 @@ module Gitlab
:parallel, :needs
attributes :script, :tags, :allow_failure, :when, :dependencies,
- :needs, :retry, :parallel, :extends, :start_in
+ :needs, :retry, :parallel, :extends, :start_in, :rules
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -151,6 +164,10 @@ module Gitlab
self.when == 'delayed'
end
+ def has_rules?
+ @config.try(:key?, :rules)
+ end
+
def ignored?
allow_failure.nil? ? manual_action? : allow_failure
end
diff --git a/lib/gitlab/ci/config/entry/rules.rb b/lib/gitlab/ci/config/entry/rules.rb
new file mode 100644
index 00000000000..65cad0880f5
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/rules.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Rules < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, presence: true
+ validates :config, type: Array
+ end
+
+ def compose!(deps = nil)
+ super(deps) do
+ @config.each_with_index do |rule, index|
+ @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Rules::Rule)
+ .value(rule)
+ .with(key: "rule", parent: self, description: "rule definition.") # rubocop:disable CodeReuse/ActiveRecord
+ .create!
+ end
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb
new file mode 100644
index 00000000000..1f2a34ec90e
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/rules/rule.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Rules::Rule < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
+
+ CLAUSES = %i[if changes].freeze
+ ALLOWED_KEYS = %i[if changes when start_in].freeze
+ ALLOWED_WHEN = %w[on_success on_failure always never manual delayed].freeze
+
+ attributes :if, :changes, :when, :start_in
+
+ validations do
+ validates :config, presence: true
+ validates :config, type: { with: Hash }
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, disallowed_keys: %i[start_in], unless: :specifies_delay?
+ validates :start_in, presence: true, if: :specifies_delay?
+ validates :start_in, duration: { limit: '1 day' }, if: :specifies_delay?
+
+ with_options allow_nil: true do
+ validates :if, expression: true
+ validates :changes, array_of_strings: true
+ validates :when, allowed_values: { in: ALLOWED_WHEN }
+ end
+ end
+
+ def specifies_delay?
+ self.when == 'delayed'
+ end
+
+ def default
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
index 942e4e55323..f7b0720d4a9 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
@@ -11,8 +11,9 @@ module Gitlab
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
+ return false unless regexp
- regexp.scan(text.to_s).any?
+ regexp.scan(text.to_s).present?
end
def self.build(_value, behind, ahead)
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
index 831c27fa0ea..02479ed28a4 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
@@ -11,8 +11,9 @@ module Gitlab
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
+ return true unless regexp
- regexp.scan(text.to_s).none?
+ regexp.scan(text.to_s).empty?
end
def self.build(_value, behind, ahead)
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 7ec03d132c0..1066331062b 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -7,7 +7,7 @@ module Gitlab
class Build < Seed::Base
include Gitlab::Utils::StrongMemoize
- delegate :dig, to: :@attributes
+ delegate :dig, to: :@seed_attributes
# When the `ci_dag_limit_needs` is enabled it uses the lower limit
LOW_NEEDS_LIMIT = 5
@@ -15,14 +15,20 @@ module Gitlab
def initialize(pipeline, attributes, previous_stages)
@pipeline = pipeline
- @attributes = attributes
+ @seed_attributes = attributes
@previous_stages = previous_stages
@needs_attributes = dig(:needs_attributes)
+ @using_rules = attributes.key?(:rules)
+ @using_only = attributes.key?(:only)
+ @using_except = attributes.key?(:except)
+
@only = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:only))
@except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
+ @rules = Gitlab::Ci::Build::Rules
+ .new(attributes.delete(:rules))
end
def name
@@ -31,8 +37,13 @@ module Gitlab
def included?
strong_memoize(:inclusion) do
- all_of_only? &&
- none_of_except?
+ if @using_rules
+ included_by_rules?
+ elsif @using_only || @using_except
+ all_of_only? && none_of_except?
+ else
+ true
+ end
end
end
@@ -45,19 +56,13 @@ module Gitlab
end
def attributes
- @attributes.merge(
- pipeline: @pipeline,
- project: @pipeline.project,
- user: @pipeline.user,
- ref: @pipeline.ref,
- tag: @pipeline.tag,
- trigger_request: @pipeline.legacy_trigger,
- protected: @pipeline.protected_ref?
- )
+ @seed_attributes
+ .deep_merge(pipeline_attributes)
+ .deep_merge(rules_attributes)
end
def bridge?
- attributes_hash = @attributes.to_h
+ attributes_hash = @seed_attributes.to_h
attributes_hash.dig(:options, :trigger).present? ||
(attributes_hash.dig(:options, :bridge_needs).instance_of?(Hash) &&
attributes_hash.dig(:options, :bridge_needs, :pipeline).present?)
@@ -73,6 +78,18 @@ module Gitlab
end
end
+ def scoped_variables_hash
+ strong_memoize(:scoped_variables_hash) do
+ # This is a temporary piece of technical debt to allow us access
+ # to the CI variables to evaluate rules before we persist a Build
+ # with the result. We should refactor away the extra Build.new,
+ # but be able to get CI Variables directly from the Seed::Build.
+ ::Ci::Build.new(
+ @seed_attributes.merge(pipeline_attributes)
+ ).scoped_variables_hash
+ end
+ end
+
private
def all_of_only?
@@ -109,6 +126,28 @@ module Gitlab
HARD_NEEDS_LIMIT
end
end
+
+ def pipeline_attributes
+ {
+ pipeline: @pipeline,
+ project: @pipeline.project,
+ user: @pipeline.user,
+ ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: @pipeline.legacy_trigger,
+ protected: @pipeline.protected_ref?
+ }
+ end
+
+ def included_by_rules?
+ rules_attributes[:when] != 'never'
+ end
+
+ def rules_attributes
+ strong_memoize(:rules_attributes) do
+ @using_rules ? @rules.evaluate(@pipeline, self).to_h.compact : {}
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 4190de73e1f..90278122361 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -46,11 +46,14 @@ sast:
SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
SAST_RUN_ANALYZER_TIMEOUT \
+ SAST_JAVA_VERSION \
ANT_HOME \
ANT_PATH \
GRADLE_PATH \
JAVA_OPTS \
JAVA_PATH \
+ JAVA_8_VERSION \
+ JAVA_11_VERSION \
MAVEN_CLI_OPTS \
MAVEN_PATH \
MAVEN_REPO_PATH \
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index 0289e675c6b..374f929878e 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -20,8 +20,10 @@ module Gitlab
present_keys = value.try(:keys).to_a & options[:in]
if present_keys.any?
- record.errors.add(attribute, "contains disallowed keys: " +
- present_keys.join(', '))
+ message = options[:message] || "contains disallowed keys"
+ message += ": #{present_keys.join(', ')}"
+
+ record.errors.add(attribute, message)
end
end
end
@@ -65,6 +67,16 @@ module Gitlab
end
end
+ class ArrayOfHashesValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Array) && value.map { |hsh| hsh.is_a?(Hash) }.all?
+ record.errors.add(attribute, 'should be an array of hashes')
+ end
+ end
+ end
+
class ArrayOrStringValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless value.is_a?(Array) || value.is_a?(String)
@@ -231,6 +243,14 @@ module Gitlab
end
end
+ class ExpressionValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(String) && ::Gitlab::Ci::Pipeline::Expression::Statement.new(value).valid?
+ record.errors.add(attribute, 'Invalid expression syntax')
+ end
+ end
+ end
+
class PortNamePresentAndUniqueValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return unless value.is_a?(Array)
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index 6d5fc4219fb..2f4ae010e74 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -46,7 +46,10 @@ module Gitlab
if thread
thread.wakeup if thread.alive?
- thread.join unless Thread.current == thread
+ begin
+ thread.join unless Thread.current == thread
+ rescue Exception # rubocop:disable Lint/RescueException
+ end
@thread = nil
end
end
diff --git a/lib/gitlab/grape_logging/loggers/client_env_logger.rb b/lib/gitlab/grape_logging/loggers/client_env_logger.rb
new file mode 100644
index 00000000000..3acc6f6a2ef
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/client_env_logger.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+# This is a fork of
+# https://github.com/aserafin/grape_logging/blob/master/lib/grape_logging/loggers/client_env.rb
+# to use remote_ip instead of ip.
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class ClientEnvLogger < ::GrapeLogging::Loggers::Base
+ def parameters(request, _)
+ { remote_ip: request.env["HTTP_X_FORWARDED_FOR"] || request.env["REMOTE_ADDR"], ua: request.env["HTTP_USER_AGENT"] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb
index e40c366ed02..75503ee1789 100644
--- a/lib/gitlab/repository_cache_adapter.rb
+++ b/lib/gitlab/repository_cache_adapter.rb
@@ -23,6 +23,37 @@ module Gitlab
end
end
+ # Caches and strongly memoizes the method as a Redis Set.
+ #
+ # This only works for methods that do not take any arguments. The method
+ # should return an Array of Strings to be cached.
+ #
+ # In addition to overriding the named method, a "name_include?" method is
+ # defined. This uses the "SISMEMBER" query to efficiently check membership
+ # without needing to load the entire set into memory.
+ #
+ # name - The name of the method to be cached.
+ # fallback - A value to fall back to if the repository does not exist, or
+ # in case of a Git error. Defaults to nil.
+ def cache_method_as_redis_set(name, fallback: nil)
+ uncached_name = alias_uncached_method(name)
+
+ define_method(name) do
+ cache_method_output_as_redis_set(name, fallback: fallback) do
+ __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ define_method("#{name}_include?") do |value|
+ # If the cache isn't populated, we can't rely on it
+ return redis_set_cache.include?(name, value) if redis_set_cache.exist?(name)
+
+ # Since we have to pull all branch names to populate the cache, use
+ # the data we already have to answer the query just this once
+ __send__(name).include?(value) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
# Caches truthy values from the method. All values are strongly memoized,
# and cached in RequestStore.
#
@@ -84,6 +115,11 @@ module Gitlab
raise NotImplementedError
end
+ # RepositorySetCache to be used. Should be overridden by the including class
+ def redis_set_cache
+ raise NotImplementedError
+ end
+
# List of cached methods. Should be overridden by the including class
def cached_methods
raise NotImplementedError
@@ -100,6 +136,18 @@ module Gitlab
end
end
+ # Caches and strongly memoizes the supplied block as a Redis Set. The result
+ # will be provided as a sorted array.
+ #
+ # name - The name of the method to be cached.
+ # fallback - A value to fall back to if the repository does not exist, or
+ # in case of a Git error. Defaults to nil.
+ def cache_method_output_as_redis_set(name, fallback: nil, &block)
+ memoize_method_output(name, fallback: fallback) do
+ redis_set_cache.fetch(name, &block).sort
+ end
+ end
+
# Caches truthy values from the supplied block. All values are strongly
# memoized, and cached in RequestStore.
#
@@ -154,6 +202,7 @@ module Gitlab
clear_memoization(memoizable_name(name))
end
+ expire_redis_set_method_caches(methods)
expire_request_store_method_caches(methods)
end
@@ -169,6 +218,10 @@ module Gitlab
end
end
+ def expire_redis_set_method_caches(methods)
+ methods.each { |name| redis_set_cache.expire(name) }
+ end
+
# All cached repository methods depend on the existence of a Git repository,
# so if the repository doesn't exist, we already know not to call it.
def fallback_early?(method_name)
diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb
new file mode 100644
index 00000000000..fb634328a95
--- /dev/null
+++ b/lib/gitlab/repository_set_cache.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+# Interface to the Redis-backed cache store for keys that use a Redis set
+module Gitlab
+ class RepositorySetCache
+ attr_reader :repository, :namespace, :expires_in
+
+ def initialize(repository, extra_namespace: nil, expires_in: 2.weeks)
+ @repository = repository
+ @namespace = "#{repository.full_path}:#{repository.project.id}"
+ @namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace
+ @expires_in = expires_in
+ end
+
+ def cache_key(type)
+ [type, namespace, 'set'].join(':')
+ end
+
+ def expire(key)
+ with { |redis| redis.del(cache_key(key)) }
+ end
+
+ def exist?(key)
+ with { |redis| redis.exists(cache_key(key)) }
+ end
+
+ def read(key)
+ with { |redis| redis.smembers(cache_key(key)) }
+ end
+
+ def write(key, value)
+ full_key = cache_key(key)
+
+ with do |redis|
+ redis.multi do
+ redis.del(full_key)
+
+ # Splitting into groups of 1000 prevents us from creating a too-long
+ # Redis command
+ value.in_groups_of(1000, false) { |subset| redis.sadd(full_key, subset) }
+
+ redis.expire(full_key, expires_in)
+ end
+ end
+
+ value
+ end
+
+ def fetch(key, &block)
+ if exist?(key)
+ read(key)
+ else
+ write(key, yield)
+ end
+ end
+
+ def include?(key, value)
+ with { |redis| redis.sismember(cache_key(key), value) }
+ end
+
+ private
+
+ def with(&blk)
+ Gitlab::Redis::Cache.with(&blk) # rubocop:disable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 764db14d720..005cb3112b8 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -39,9 +39,14 @@ module Gitlab
# development and test. If you need development and test to behave
# just the same as production you can use this instead of
# track_exception.
+ #
+ # If the exception implements the method `sentry_extra_data` and that method
+ # returns a Hash, then the return value of that method will be merged into
+ # `extra`. Exceptions can use this mechanism to provide structured data
+ # to sentry in addition to their message and back-trace.
def self.track_acceptable_exception(exception, issue_url: nil, extra: {})
if enabled?
- extra[:issue_url] = issue_url if issue_url
+ extra = build_extra_data(exception, issue_url, extra)
context # Make sure we've set everything we know in the context
Raven.capture_exception(exception, tags: default_tags, extra: extra)
@@ -58,5 +63,15 @@ module Gitlab
locale: I18n.locale
}
end
+
+ def self.build_extra_data(exception, issue_url, extra)
+ exception.try(:sentry_extra_data)&.tap do |data|
+ extra.merge!(data) if data.is_a?(Hash)
+ end
+
+ extra.merge({ issue_url: issue_url }.compact)
+ end
+
+ private_class_method :build_extra_data
end
end
diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb
index b06ffa9c121..3dc9521ee8b 100644
--- a/lib/gitlab/sidekiq_middleware/metrics.rb
+++ b/lib/gitlab/sidekiq_middleware/metrics.rb
@@ -3,6 +3,10 @@
module Gitlab
module SidekiqMiddleware
class Metrics
+ # SIDEKIQ_LATENCY_BUCKETS are latency histogram buckets better suited to Sidekiq
+ # timeframes than the DEFAULT_BUCKET definition. Defined in seconds.
+ SIDEKIQ_LATENCY_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60, 300, 600].freeze
+
def initialize
@metrics = init_metrics
end
@@ -31,7 +35,7 @@ module Gitlab
def init_metrics
{
- sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job'),
+ sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job', buckets: SIDEKIQ_LATENCY_BUCKETS),
sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'),
sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'),
sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :livesum)
diff --git a/lib/gitlab/sidekiq_middleware/monitor.rb b/lib/gitlab/sidekiq_middleware/monitor.rb
new file mode 100644
index 00000000000..0d88fe760d3
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/monitor.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ class Monitor
+ def call(worker, job, queue)
+ Gitlab::SidekiqMonitor.instance.within_job(job['jid'], queue) do
+ yield
+ end
+ rescue Gitlab::SidekiqMonitor::CancelledError
+ # ignore retries
+ raise Sidekiq::JobRetry::Skip
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_monitor.rb b/lib/gitlab/sidekiq_monitor.rb
new file mode 100644
index 00000000000..9842f1f53f7
--- /dev/null
+++ b/lib/gitlab/sidekiq_monitor.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class SidekiqMonitor < Daemon
+ include ::Gitlab::Utils::StrongMemoize
+
+ NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications'.freeze
+ CANCEL_DEADLINE = 24.hours.seconds
+ RECONNECT_TIME = 3.seconds
+
+ # We use exception derived from `Exception`
+ # to consider this as an very low-level exception
+ # that should not be caught by application
+ CancelledError = Class.new(Exception) # rubocop:disable Lint/InheritException
+
+ attr_reader :jobs_thread
+ attr_reader :jobs_mutex
+
+ def initialize
+ super
+
+ @jobs_thread = {}
+ @jobs_mutex = Mutex.new
+ end
+
+ def within_job(jid, queue)
+ jobs_mutex.synchronize do
+ jobs_thread[jid] = Thread.current
+ end
+
+ if cancelled?(jid)
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'run',
+ queue: queue,
+ jid: jid,
+ canceled: true
+ )
+ raise CancelledError
+ end
+
+ yield
+ ensure
+ jobs_mutex.synchronize do
+ jobs_thread.delete(jid)
+ end
+ end
+
+ def self.cancel_job(jid)
+ payload = {
+ action: 'cancel',
+ jid: jid
+ }.to_json
+
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.setex(cancel_job_key(jid), CANCEL_DEADLINE, 1)
+ redis.publish(NOTIFICATION_CHANNEL, payload)
+ end
+ end
+
+ private
+
+ def start_working
+ Sidekiq.logger.info(
+ class: self.class.to_s,
+ action: 'start',
+ message: 'Starting Monitor Daemon'
+ )
+
+ while enabled?
+ process_messages
+ sleep(RECONNECT_TIME)
+ end
+
+ ensure
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'stop',
+ message: 'Stopping Monitor Daemon'
+ )
+ end
+
+ def stop_working
+ thread.raise(Interrupt) if thread.alive?
+ end
+
+ def process_messages
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.subscribe(NOTIFICATION_CHANNEL) do |on|
+ on.message do |channel, message|
+ process_message(message)
+ end
+ end
+ end
+ rescue Exception => e # rubocop:disable Lint/RescueException
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'exception',
+ message: e.message
+ )
+
+ # we re-raise system exceptions
+ raise e unless e.is_a?(StandardError)
+ end
+
+ def process_message(message)
+ Sidekiq.logger.info(
+ class: self.class.to_s,
+ channel: NOTIFICATION_CHANNEL,
+ message: 'Received payload on channel',
+ payload: message
+ )
+
+ message = safe_parse(message)
+ return unless message
+
+ case message['action']
+ when 'cancel'
+ process_job_cancel(message['jid'])
+ else
+ # unknown message
+ end
+ end
+
+ def safe_parse(message)
+ JSON.parse(message)
+ rescue JSON::ParserError
+ end
+
+ def process_job_cancel(jid)
+ return unless jid
+
+ # try to find thread without lock
+ return unless find_thread_unsafe(jid)
+
+ Thread.new do
+ # try to find a thread, but with guaranteed
+ # that handle for thread corresponds to actually
+ # running job
+ find_thread_with_lock(jid) do |thread|
+ Sidekiq.logger.warn(
+ class: self.class.to_s,
+ action: 'cancel',
+ message: 'Canceling thread with CancelledError',
+ jid: jid,
+ thread_id: thread.object_id
+ )
+
+ thread&.raise(CancelledError)
+ end
+ end
+ end
+
+ # This method needs to be thread-safe
+ # This is why it passes thread in block,
+ # to ensure that we do process this thread
+ def find_thread_unsafe(jid)
+ jobs_thread[jid]
+ end
+
+ def find_thread_with_lock(jid)
+ # don't try to lock if we cannot find the thread
+ return unless find_thread_unsafe(jid)
+
+ jobs_mutex.synchronize do
+ find_thread_unsafe(jid).tap do |thread|
+ yield(thread) if thread
+ end
+ end
+ end
+
+ def cancelled?(jid)
+ ::Gitlab::Redis::SharedState.with do |redis|
+ redis.exists(self.class.cancel_job_key(jid))
+ end
+ end
+
+ def self.cancel_job_key(jid)
+ "sidekiq:cancel:#{jid}"
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data_counters/note_counter.rb b/lib/gitlab/usage_data_counters/note_counter.rb
index e93a0bcfa27..672450ec82b 100644
--- a/lib/gitlab/usage_data_counters/note_counter.rb
+++ b/lib/gitlab/usage_data_counters/note_counter.rb
@@ -4,7 +4,7 @@ module Gitlab::UsageDataCounters
class NoteCounter < BaseCounter
KNOWN_EVENTS = %w[create].freeze
PREFIX = 'note'
- COUNTABLE_TYPES = %w[Snippet].freeze
+ COUNTABLE_TYPES = %w[Snippet Commit MergeRequest].freeze
class << self
def redis_key(event, noteable_type)
@@ -24,9 +24,9 @@ module Gitlab::UsageDataCounters
end
def totals
- {
- snippet_comment: read(:create, 'Snippet')
- }
+ COUNTABLE_TYPES.map do |countable_type|
+ [:"#{countable_type.underscore}_comment", read(:create, countable_type)]
+ end.to_h
end
private
diff --git a/lib/gt_one_coercion.rb b/lib/gt_one_coercion.rb
deleted file mode 100644
index 99be51bc8c6..00000000000
--- a/lib/gt_one_coercion.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class GtOneCoercion < Virtus::Attribute
- def coerce(value)
- [1, value.to_i].max
- end
-end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d84203ee911..1c1a3a51932 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -128,9 +128,6 @@ msgstr[1] ""
msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
-msgid "%{canMergeCount}/%{assigneesCount} can merge"
-msgstr ""
-
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
@@ -202,6 +199,9 @@ msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
+msgid "%{mergeLength}/%{usersLength} can merge"
+msgstr ""
+
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
@@ -279,6 +279,9 @@ msgstr ""
msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc."
msgstr ""
+msgid "%{userName} (cannot merge)"
+msgstr ""
+
msgid "%{userName}'s avatar"
msgstr ""
@@ -306,6 +309,9 @@ msgstr ""
msgid "(external source)"
msgstr ""
+msgid "+ %{amount} more"
+msgstr ""
+
msgid "+ %{count} more"
msgstr ""
@@ -7439,9 +7445,6 @@ msgstr ""
msgid "No milestones to show"
msgstr ""
-msgid "No one can merge"
-msgstr ""
-
msgid "No other labels with such name or description"
msgstr ""
@@ -8020,6 +8023,9 @@ msgstr ""
msgid "Pipelines for last year"
msgstr ""
+msgid "Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results."
+msgstr ""
+
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
@@ -11160,9 +11166,6 @@ msgstr ""
msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git."
msgstr ""
-msgid "The code of a detached pipeline is tested against the source branch instead of merged results"
-msgstr ""
-
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr ""
@@ -12888,15 +12891,9 @@ msgstr ""
msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}"
msgstr ""
-msgid "WikiNewPagePlaceholder|how-to-setup"
-msgstr ""
-
msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories."
msgstr ""
-msgid "WikiNewPageTitle|New Wiki Page"
-msgstr ""
-
msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr ""
@@ -12912,19 +12909,16 @@ msgstr ""
msgid "WikiPageConflictMessage|the page"
msgstr ""
-msgid "WikiPageCreate|Create %{page_title}"
-msgstr ""
-
-msgid "WikiPageEdit|Update %{page_title}"
+msgid "WikiPageCreate|Create %{pageTitle}"
msgstr ""
-msgid "WikiPage|Page slug"
+msgid "WikiPageEdit|Update %{pageTitle}"
msgstr ""
msgid "WikiPage|Write your content or drag files here…"
msgstr ""
-msgid "Wiki|Create Page"
+msgid "Wiki|Create New Page"
msgstr ""
msgid "Wiki|Create page"
@@ -12945,6 +12939,9 @@ msgstr ""
msgid "Wiki|Page history"
msgstr ""
+msgid "Wiki|Page title"
+msgstr ""
+
msgid "Wiki|Page version"
msgstr ""
@@ -13401,18 +13398,15 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
-msgid "a Zoom call was added to this issue"
-msgstr ""
-
-msgid "a Zoom call was removed from this issue"
-msgstr ""
-
msgid "a deleted user"
msgstr ""
msgid "added %{created_at_timeago}"
msgstr ""
+msgid "added a Zoom call to this issue"
+msgstr ""
+
msgid "ago"
msgstr ""
@@ -13452,6 +13446,9 @@ msgstr ""
msgid "cannot include leading slash or directory traversal."
msgstr ""
+msgid "cannot merge"
+msgstr ""
+
msgid "comment"
msgstr ""
@@ -13879,6 +13876,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no one can merge"
+msgstr ""
+
msgid "none"
msgstr ""
@@ -13949,6 +13949,9 @@ msgstr ""
msgid "remove due date"
msgstr ""
+msgid "removed a Zoom call from this issue"
+msgstr ""
+
msgid "rendered diff"
msgstr ""
diff --git a/package.json b/package.json
index 2b9a00d1cbd..97ea872ee61 100644
--- a/package.json
+++ b/package.json
@@ -30,13 +30,13 @@
"webpack-vrt": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.review_toolbar.js"
},
"dependencies": {
- "@babel/core": "^7.4.4",
- "@babel/plugin-proposal-class-properties": "^7.4.4",
+ "@babel/core": "^7.5.5",
+ "@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-json-strings": "^7.2.0",
"@babel/plugin-proposal-private-methods": "^7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
- "@babel/preset-env": "^7.4.4",
+ "@babel/preset-env": "^7.5.5",
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.68.0",
"@gitlab/ui": "5.15.0",
@@ -145,7 +145,7 @@
"xterm": "^3.5.0"
},
"devDependencies": {
- "@babel/plugin-transform-modules-commonjs": "^7.2.0",
+ "@babel/plugin-transform-modules-commonjs": "^7.5.0",
"@gitlab/eslint-config": "^1.6.0",
"@gitlab/eslint-plugin-i18n": "^1.1.0",
"@gitlab/eslint-plugin-vue-i18n": "^1.2.0",
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
index f51c16f472c..1c1f552e224 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
@@ -19,7 +19,7 @@ module QA
page.add_member(user.username)
end
- expect(page).to have_content(/#{user.name} (. )?@#{user.username} Given access/)
+ expect(page).to have_content(/@#{user.username}(\n| )?Given access/)
end
end
end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index f210537aad5..7bdf5c49425 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -24,78 +24,6 @@ describe IssuableCollections do
controller
end
- describe '#set_sort_order_from_user_preference' do
- describe 'when sort param given' do
- let(:params) { { sort: 'updated_desc' } }
-
- context 'when issuable_sorting_field is defined' do
- before do
- controller.class.define_method(:issuable_sorting_field) { :issues_sort}
- end
-
- it 'sets user_preference with the right value' do
- controller.send(:set_sort_order_from_user_preference)
-
- expect(user.user_preference.reload.issues_sort).to eq('updated_desc')
- end
- end
-
- context 'when no issuable_sorting_field is defined on the controller' do
- it 'does not touch user_preference' do
- allow(user).to receive(:user_preference)
-
- controller.send(:set_sort_order_from_user_preference)
-
- expect(user).not_to have_received(:user_preference)
- end
- end
- end
-
- context 'when a user sorting preference exists' do
- let(:params) { {} }
-
- before do
- controller.class.define_method(:issuable_sorting_field) { :issues_sort }
- end
-
- it 'returns the set preference' do
- user.user_preference.update(issues_sort: 'updated_asc')
-
- sort_preference = controller.send(:set_sort_order_from_user_preference)
-
- expect(sort_preference).to eq('updated_asc')
- end
- end
- end
-
- describe '#set_set_order_from_cookie' do
- describe 'when sort param given' do
- let(:cookies) { {} }
- let(:params) { { sort: 'downvotes_asc' } }
-
- it 'sets the cookie with the right values and flags' do
- allow(controller).to receive(:cookies).and_return(cookies)
-
- controller.send(:set_sort_order_from_cookie)
-
- expect(cookies['issue_sort']).to eq({ value: 'popularity', secure: false, httponly: false })
- end
- end
-
- describe 'when cookie exists' do
- let(:cookies) { { 'issue_sort' => 'id_asc' } }
- let(:params) { {} }
-
- it 'sets the cookie with the right values and flags' do
- allow(controller).to receive(:cookies).and_return(cookies)
-
- controller.send(:set_sort_order_from_cookie)
-
- expect(cookies['issue_sort']).to eq({ value: 'created_asc', secure: false, httponly: false })
- end
- end
- end
-
describe '#page_count_for_relation' do
let(:params) { { state: 'opened' } }
diff --git a/spec/controllers/concerns/sorting_preference_spec.rb b/spec/controllers/concerns/sorting_preference_spec.rb
new file mode 100644
index 00000000000..a36124c6776
--- /dev/null
+++ b/spec/controllers/concerns/sorting_preference_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe SortingPreference do
+ let(:user) { create(:user) }
+
+ let(:controller_class) do
+ Class.new do
+ def self.helper_method(name); end
+
+ include SortingPreference
+ include SortingHelper
+ end
+ end
+
+ let(:controller) { controller_class.new }
+
+ before do
+ allow(controller).to receive(:params).and_return(ActionController::Parameters.new(params))
+ allow(controller).to receive(:current_user).and_return(user)
+ allow(controller).to receive(:legacy_sort_cookie_name).and_return('issuable_sort')
+ allow(controller).to receive(:sorting_field).and_return(:issues_sort)
+ end
+
+ describe '#set_sort_order_from_user_preference' do
+ subject { controller.send(:set_sort_order_from_user_preference) }
+
+ context 'when sort param given' do
+ let(:params) { { sort: 'updated_desc' } }
+
+ context 'when sorting_field is defined' do
+ it 'sets user_preference with the right value' do
+ is_expected.to eq('updated_desc')
+ end
+ end
+
+ context 'when no sorting_field is defined on the controller' do
+ before do
+ allow(controller).to receive(:sorting_field).and_return(nil)
+ end
+
+ it 'does not touch user_preference' do
+ expect(user).not_to receive(:user_preference)
+
+ subject
+ end
+ end
+ end
+
+ context 'when a user sorting preference exists' do
+ let(:params) { {} }
+
+ before do
+ user.user_preference.update!(issues_sort: 'updated_asc')
+ end
+
+ it 'returns the set preference' do
+ is_expected.to eq('updated_asc')
+ end
+ end
+ end
+
+ describe '#set_set_order_from_cookie' do
+ subject { controller.send(:set_sort_order_from_cookie) }
+
+ before do
+ allow(controller).to receive(:cookies).and_return(cookies)
+ end
+
+ context 'when sort param given' do
+ let(:cookies) { {} }
+ let(:params) { { sort: 'downvotes_asc' } }
+
+ it 'sets the cookie with the right values and flags' do
+ subject
+
+ expect(cookies['issue_sort']).to eq(value: 'popularity', secure: false, httponly: false)
+ end
+ end
+
+ context 'when cookie exists' do
+ let(:cookies) { { 'issue_sort' => 'id_asc' } }
+ let(:params) { {} }
+
+ it 'sets the cookie with the right values and flags' do
+ subject
+
+ expect(cookies['issue_sort']).to eq(value: 'created_asc', secure: false, httponly: false)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index 6591901a9dc..8b95c9f2496 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -40,6 +40,14 @@ describe Dashboard::ProjectsController do
expect(assigns(:projects)).to eq([project, project2])
end
+
+ context 'project sorting' do
+ let(:project) { create(:project) }
+
+ it_behaves_like 'set sort order from user preference' do
+ let(:sorting_param) { 'created_asc' }
+ end
+ end
end
end
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index 463586ee422..6752d2b8ebd 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -3,56 +3,91 @@
require 'spec_helper'
describe Explore::ProjectsController do
- describe 'GET #index.json' do
- render_views
+ shared_examples 'explore projects' do
+ describe 'GET #index.json' do
+ render_views
- before do
- get :index, format: :json
+ before do
+ get :index, format: :json
+ end
+
+ it { is_expected.to respond_with(:success) }
end
- it { is_expected.to respond_with(:success) }
- end
+ describe 'GET #trending.json' do
+ render_views
- describe 'GET #trending.json' do
- render_views
+ before do
+ get :trending, format: :json
+ end
- before do
- get :trending, format: :json
+ it { is_expected.to respond_with(:success) }
+ end
+
+ describe 'GET #starred.json' do
+ render_views
+
+ before do
+ get :starred, format: :json
+ end
+
+ it { is_expected.to respond_with(:success) }
end
- it { is_expected.to respond_with(:success) }
+ describe 'GET #trending' do
+ context 'sorting by update date' do
+ let(:project1) { create(:project, :public, updated_at: 3.days.ago) }
+ let(:project2) { create(:project, :public, updated_at: 1.day.ago) }
+
+ before do
+ create(:trending_project, project: project1)
+ create(:trending_project, project: project2)
+ end
+
+ it 'sorts by last updated' do
+ get :trending, params: { sort: 'updated_desc' }
+
+ expect(assigns(:projects)).to eq [project2, project1]
+ end
+
+ it 'sorts by oldest updated' do
+ get :trending, params: { sort: 'updated_asc' }
+
+ expect(assigns(:projects)).to eq [project1, project2]
+ end
+ end
+ end
end
- describe 'GET #starred.json' do
- render_views
+ context 'when user is signed in' do
+ let(:user) { create(:user) }
before do
- get :starred, format: :json
+ sign_in(user)
end
- it { is_expected.to respond_with(:success) }
- end
+ include_examples 'explore projects'
- describe 'GET #trending' do
- context 'sorting by update date' do
- let(:project1) { create(:project, :public, updated_at: 3.days.ago) }
- let(:project2) { create(:project, :public, updated_at: 1.day.ago) }
+ context 'user preference sorting' do
+ let(:project) { create(:project) }
- before do
- create(:trending_project, project: project1)
- create(:trending_project, project: project2)
+ it_behaves_like 'set sort order from user preference' do
+ let(:sorting_param) { 'created_asc' }
end
+ end
+ end
- it 'sorts by last updated' do
- get :trending, params: { sort: 'updated_desc' }
+ context 'when user is not signed in' do
+ include_examples 'explore projects'
- expect(assigns(:projects)).to eq [project2, project1]
- end
+ context 'user preference sorting' do
+ let(:project) { create(:project) }
+ let(:sorting_param) { 'created_asc' }
- it 'sorts by oldest updated' do
- get :trending, params: { sort: 'updated_asc' }
+ it 'does not set sort order from user preference' do
+ expect_any_instance_of(UserPreference).not_to receive(:update)
- expect(assigns(:projects)).to eq [project1, project2]
+ get :index, params: { sort: sorting_param }
end
end
end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 908c564e761..0c3dd971582 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -172,7 +172,7 @@ describe Groups::GroupMembersController do
it '[JS] removes user from members' do
delete :destroy, params: { group_id: group, id: member }, xhr: true
- expect(response).to be_success
+ expect(response).to be_successful
expect(group.members).not_to include member
end
end
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index bf164aeed38..41927907fd1 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -186,7 +186,7 @@ describe Groups::MilestonesController do
it "removes milestone" do
delete :destroy, params: { group_id: group.to_param, id: milestone.iid }, format: :js
- expect(response).to be_success
+ expect(response).to be_successful
expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound)
end
end
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index 92f005faf4a..b48b7dc86e0 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -33,14 +33,14 @@ describe HealthCheckController do
get :index
- expect(response).to be_success
+ expect(response).to be_successful
expect(response.content_type).to eq 'text/plain'
end
it 'supports passing the token in query params' do
get :index, params: { token: token }
- expect(response).to be_success
+ expect(response).to be_successful
expect(response.content_type).to eq 'text/plain'
end
end
@@ -54,14 +54,14 @@ describe HealthCheckController do
it 'supports successful plaintext response' do
get :index
- expect(response).to be_success
+ expect(response).to be_successful
expect(response.content_type).to eq 'text/plain'
end
it 'supports successful json response' do
get :index, format: :json
- expect(response).to be_success
+ expect(response).to be_successful
expect(response.content_type).to eq 'application/json'
expect(json_response['healthy']).to be true
end
@@ -69,7 +69,7 @@ describe HealthCheckController do
it 'supports successful xml response' do
get :index, format: :xml
- expect(response).to be_success
+ expect(response).to be_successful
expect(response.content_type).to eq 'application/xml'
expect(xml_response['healthy']).to be true
end
@@ -77,7 +77,7 @@ describe HealthCheckController do
it 'supports successful responses for specific checks' do
get :index, params: { checks: 'email' }, format: :json
- expect(response).to be_success
+ expect(response).to be_successful
expect(response.content_type).to eq 'application/json'
expect(json_response['healthy']).to be true
end
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index 43c910da7a5..03b6e85b653 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -114,7 +114,7 @@ describe HelpController do
path: 'user/project/img/labels_default_v12_1'
},
format: :png
- expect(response).to be_success
+ expect(response).to be_successful
expect(response.content_type).to eq 'image/png'
expect(response.headers['Content-Disposition']).to match(/^inline;/)
end
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index 753eb432c5e..3bed117deb0 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -10,7 +10,7 @@ describe Profiles::KeysController do
it "does not generally work" do
get :get_keys, params: { username: 'not-existent' }
- expect(response).not_to be_success
+ expect(response).not_to be_successful
end
end
@@ -18,7 +18,7 @@ describe Profiles::KeysController do
it "does generally work" do
get :get_keys, params: { username: user.username }
- expect(response).to be_success
+ expect(response).to be_successful
end
it "renders all keys separated with a new line" do
@@ -41,7 +41,7 @@ describe Profiles::KeysController do
it "does generally work" do
get :get_keys, params: { username: user.username }
- expect(response).to be_success
+ expect(response).to be_successful
end
it "renders all non deploy keys separated with a new line" do
diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb
index 96e82b7086c..14128fb5b0e 100644
--- a/spec/controllers/projects/ci/lints_controller_spec.rb
+++ b/spec/controllers/projects/ci/lints_controller_spec.rb
@@ -20,9 +20,7 @@ describe Projects::Ci::LintsController do
get :show, params: { namespace_id: project.namespace, project_id: project }
end
- it 'is success' do
- expect(response).to be_success
- end
+ it { expect(response).to be_successful }
it 'renders show page' do
expect(response).to render_template :show
@@ -78,9 +76,7 @@ describe Projects::Ci::LintsController do
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
end
- it 'is success' do
- expect(response).to be_success
- end
+ it { expect(response).to be_successful }
it 'render show page' do
expect(response).to render_template :show
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 58a1d96d010..afd5cb15e0f 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -45,14 +45,14 @@ describe Projects::CommitController do
it 'handles binary files' do
go(id: TestEnv::BRANCH_SHA['binary-encoding'], format: 'html')
- expect(response).to be_success
+ expect(response).to be_successful
end
shared_examples "export as" do |format|
it "does generally work" do
go(id: commit.id, format: format)
- expect(response).to be_success
+ expect(response).to be_successful
end
it "generates it" do
@@ -110,7 +110,7 @@ describe Projects::CommitController do
id: commit.id
})
- expect(response).to be_success
+ expect(response).to be_successful
end
end
@@ -177,7 +177,7 @@ describe Projects::CommitController do
id: commit.id
})
- expect(response).not_to be_success
+ expect(response).not_to be_successful
expect(response).to have_gitlab_http_status(404)
end
end
@@ -234,7 +234,7 @@ describe Projects::CommitController do
id: master_pickable_commit.id
})
- expect(response).not_to be_success
+ expect(response).not_to be_successful
expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 9db1ac2a46c..9c4d6fdcb2a 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -79,7 +79,7 @@ describe Projects::CommitsController do
end
it "renders as atom" do
- expect(response).to be_success
+ expect(response).to be_successful
expect(response.content_type).to eq('application/atom+xml')
end
@@ -104,7 +104,7 @@ describe Projects::CommitsController do
end
it "renders as HTML" do
- expect(response).to be_success
+ expect(response).to be_successful
expect(response.content_type).to eq('text/html')
end
end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 48a92a772dc..9afc46c4be9 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::CompareController do
end
it 'returns successfully' do
- expect(response).to be_success
+ expect(response).to be_successful
end
end
@@ -49,7 +49,7 @@ describe Projects::CompareController do
it 'shows some diffs with ignore whitespace change option' do
show_request
- expect(response).to be_success
+ expect(response).to be_successful
diff_file = assigns(:diffs).diff_files.first
expect(diff_file).not_to be_nil
expect(assigns(:commits).length).to be >= 1
@@ -67,7 +67,7 @@ describe Projects::CompareController do
it 'sets the diffs and commits ivars' do
show_request
- expect(response).to be_success
+ expect(response).to be_successful
expect(assigns(:diffs).diff_files.first).not_to be_nil
expect(assigns(:commits).length).to be >= 1
end
@@ -81,7 +81,7 @@ describe Projects::CompareController do
it 'sets empty diff and commit ivars' do
show_request
- expect(response).to be_success
+ expect(response).to be_successful
expect(assigns(:diffs)).to eq([])
expect(assigns(:commits)).to eq([])
end
@@ -94,7 +94,7 @@ describe Projects::CompareController do
it 'sets empty diff and commit ivars' do
show_request
- expect(response).to be_success
+ expect(response).to be_successful
expect(assigns(:diffs)).to eq([])
expect(assigns(:commits)).to eq([])
end
diff --git a/spec/controllers/projects/cycle_analytics/events_controller_spec.rb b/spec/controllers/projects/cycle_analytics/events_controller_spec.rb
index 8fc3ae0aa32..b828c678d0c 100644
--- a/spec/controllers/projects/cycle_analytics/events_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics/events_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::CycleAnalytics::EventsController do
it 'is empty' do
get_issue
- expect(response).to be_success
+ expect(response).to be_successful
expect(JSON.parse(response.body)['events']).to be_empty
end
end
@@ -32,7 +32,7 @@ describe Projects::CycleAnalytics::EventsController do
it 'is not empty' do
get_issue
- expect(response).to be_success
+ expect(response).to be_successful
end
it 'contains event detais' do
@@ -49,7 +49,7 @@ describe Projects::CycleAnalytics::EventsController do
it 'is empty' do
get_issue(additional_params: { cycle_analytics: { start_date: 7 } })
- expect(response).to be_success
+ expect(response).to be_successful
expect(JSON.parse(response.body)['events']).to be_empty
end
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 5e6ceef2517..65eee7b8ead 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -21,7 +21,7 @@ describe Projects::CycleAnalyticsController do
project_id: project
})
- expect(response).to be_success
+ expect(response).to be_successful
end
end
@@ -34,7 +34,7 @@ describe Projects::CycleAnalyticsController do
project_id: project
})
- expect(response).to be_success
+ expect(response).to be_successful
expect(assigns(:cycle_analytics_no_data)).to eq(true)
end
end
@@ -55,7 +55,7 @@ describe Projects::CycleAnalyticsController do
project_id: project
})
- expect(response).to be_success
+ expect(response).to be_successful
expect(assigns(:cycle_analytics_no_data)).to eq(false)
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index fab47aa4701..187c7864ad7 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1104,18 +1104,39 @@ describe Projects::IssuesController do
project.add_developer(user)
end
+ subject do
+ post(:toggle_award_emoji, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue.iid,
+ name: emoji_name
+ })
+ end
+ let(:emoji_name) { 'thumbsup' }
+
it "toggles the award emoji" do
expect do
- post(:toggle_award_emoji, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: issue.iid,
- name: "thumbsup"
- })
+ subject
end.to change { issue.award_emoji.count }.by(1)
expect(response).to have_gitlab_http_status(200)
end
+
+ it "removes the already awarded emoji" do
+ create(:award_emoji, awardable: issue, name: emoji_name, user: user)
+
+ expect { subject }.to change { AwardEmoji.count }.by(-1)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'marks Todos on the Issue as done' do
+ todo = create(:todo, target: issue, project: project, user: user)
+
+ subject
+
+ expect(todo.reload).to be_done
+ end
end
describe 'POST create_merge_request' do
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index 3816e1c7a31..ce977f26ec6 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -28,7 +28,7 @@ describe Projects::MergeRequests::CreationsController do
it 'renders new merge request widget template' do
get :new, params: get_diff_params
- expect(response).to be_success
+ expect(response).to be_successful
end
end
@@ -56,7 +56,7 @@ describe Projects::MergeRequests::CreationsController do
it 'limits total commits' do
get :new, params: large_diff_params
- expect(response).to be_success
+ expect(response).to be_successful
total = assigns(:total_commit_count)
expect(assigns(:commits)).to be_an Array
@@ -70,7 +70,7 @@ describe Projects::MergeRequests::CreationsController do
it 'shows total commits' do
get :new, params: large_diff_params
- expect(response).to be_success
+ expect(response).to be_successful
total = assigns(:total_commit_count)
expect(assigns(:commits)).to be_an CommitCollection
@@ -89,7 +89,7 @@ describe Projects::MergeRequests::CreationsController do
get :diffs, params: get_diff_params.merge(format: 'json')
- expect(response).to be_success
+ expect(response).to be_successful
expect(assigns[:diffs]).to be_nil
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index d940d226176..ac3e9901123 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -66,7 +66,7 @@ describe Projects::MergeRequests::DiffsController do
end
it 'renders' do
- expect(response).to be_success
+ expect(response).to be_successful
expect(response.body).to have_content('Subproject commit')
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index b1dc6a65dd4..11b1eaf11b7 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -57,7 +57,7 @@ describe Projects::MergeRequestsController do
go(format: :html)
- expect(response).to be_success
+ expect(response).to be_successful
end
end
@@ -66,7 +66,7 @@ describe Projects::MergeRequestsController do
go(format: :html)
- expect(response).to be_success
+ expect(response).to be_successful
end
context "that is invalid" do
@@ -75,7 +75,7 @@ describe Projects::MergeRequestsController do
it "renders merge request page" do
go(format: :html)
- expect(response).to be_success
+ expect(response).to be_successful
end
end
end
@@ -124,7 +124,7 @@ describe Projects::MergeRequestsController do
it "renders merge request page" do
go(format: :json)
- expect(response).to be_success
+ expect(response).to be_successful
end
end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 9b2025b836c..cbf9d437909 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -139,7 +139,7 @@ describe Projects::MilestonesController do
expect(issue.milestone_id).to eq(milestone.id)
delete :destroy, params: { namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid }, format: :js
- expect(response).to be_success
+ expect(response).to be_successful
expect(Event.recent.first.action).to eq(Event::DESTROYED)
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 9ab565dc2e8..4500c412521 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -543,23 +543,32 @@ describe Projects::NotesController do
project.add_developer(user)
end
+ subject { post(:toggle_award_emoji, params: request_params.merge(name: emoji_name)) }
+ let(:emoji_name) { 'thumbsup' }
+
it "toggles the award emoji" do
expect do
- post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup"))
+ subject
end.to change { note.award_emoji.count }.by(1)
expect(response).to have_gitlab_http_status(200)
end
it "removes the already awarded emoji" do
- post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup"))
+ create(:award_emoji, awardable: note, name: emoji_name, user: user)
- expect do
- post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup"))
- end.to change { AwardEmoji.count }.by(-1)
+ expect { subject }.to change { AwardEmoji.count }.by(-1)
expect(response).to have_gitlab_http_status(200)
end
+
+ it 'marks Todos on the Noteable as done' do
+ todo = create(:todo, target: note.noteable, project: project, user: user)
+
+ subject
+
+ expect(todo.reload).to be_done
+ end
end
describe "resolving and unresolving" do
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 9a50ea79f5e..089d06e11eb 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -177,18 +177,22 @@ describe Projects::PipelinesController do
end
it 'does not perform N + 1 queries' do
+ # Set up all required variables
+ get_pipeline_json
+
control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
- create_build('test', 1, 'rspec 1')
- create_build('test', 1, 'spinach 0')
- create_build('test', 1, 'spinach 1')
- create_build('test', 1, 'audit')
- create_build('post deploy', 3, 'pages 1')
- create_build('post deploy', 3, 'pages 2')
+ first_build = pipeline.builds.first
+ first_build.tag_list << [:hello, :world]
+ create(:deployment, deployable: first_build)
+
+ second_build = pipeline.builds.second
+ second_build.tag_list << [:docker, :ruby]
+ create(:deployment, deployable: second_build)
new_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count
- expect(new_count).to be_within(12).of(control_count)
+ expect(new_count).to be_within(1).of(control_count)
end
end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 4141e41c7a7..5130e26c928 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -158,7 +158,7 @@ describe Projects::ProjectMembersController do
id: member
}, xhr: true
- expect(response).to be_success
+ expect(response).to be_successful
expect(project.members).not_to include member
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index b958f419a19..8b43d1264b2 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -67,9 +67,9 @@ describe Projects::RawController do
attributes = {
message: 'Action_Rate_Limiter_Request',
env: :raw_blob_request_limit,
- ip: '0.0.0.0',
+ remote_ip: '0.0.0.0',
request_method: 'GET',
- fullpath: "/#{project.full_path}/raw/#{file_path}"
+ path: "/#{project.full_path}/raw/#{file_path}"
}
expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index 6db98f2428b..646c7a7db7c 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -49,7 +49,7 @@ describe Projects::RefsController do
expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
xhr_get(:js)
- expect(response).to be_success
+ expect(response).to be_successful
end
it 'renders JSON' do
@@ -57,7 +57,7 @@ describe Projects::RefsController do
xhr_get(:json)
- expect(response).to be_success
+ expect(response).to be_successful
expect(json_response).to be_kind_of(Array)
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 68eabce8513..563b61962cf 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -159,7 +159,7 @@ describe Projects::ServicesController do
context 'with approved services' do
it 'renders edit page' do
- expect(response).to be_success
+ expect(response).to be_successful
end
end
end
diff --git a/spec/controllers/projects/starrers_controller_spec.rb b/spec/controllers/projects/starrers_controller_spec.rb
index 7085cba08d5..5774ff7c576 100644
--- a/spec/controllers/projects/starrers_controller_spec.rb
+++ b/spec/controllers/projects/starrers_controller_spec.rb
@@ -32,6 +32,20 @@ describe Projects::StarrersController do
end
end
+ context 'N+1 queries' do
+ render_views
+
+ it 'avoids N+1s loading users', :request_store do
+ get_starrers
+
+ control_count = ActiveRecord::QueryRecorder.new { get_starrers }.count
+
+ create_list(:user, 5).each { |user| user.toggle_star(project) }
+
+ expect { get_starrers }.not_to exceed_query_limit(control_count)
+ end
+ end
+
context 'when project is public' do
before do
project.update_attribute(:visibility_level, Project::PUBLIC)
diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb
index fbca1d5740f..6fea6bca4f2 100644
--- a/spec/controllers/projects/wikis_controller_spec.rb
+++ b/spec/controllers/projects/wikis_controller_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
describe Projects::WikisController do
- let(:project) { create(:project, :public, :repository) }
- let(:user) { project.owner }
+ set(:project) { create(:project, :public, :repository) }
+ set(:user) { project.owner }
let(:project_wiki) { ProjectWiki.new(project, user) }
let(:wiki) { project_wiki.wiki }
- let(:wiki_title) { 'page-title-test' }
+ let(:wiki_title) { 'page title test' }
before do
create_page(wiki_title, 'hello world')
@@ -19,6 +19,21 @@ describe Projects::WikisController do
destroy_page(wiki_title)
end
+ describe 'GET #new' do
+ subject { get :new, params: { namespace_id: project.namespace, project_id: project } }
+
+ it 'redirects to #show and appends a `random_title` param' do
+ subject
+
+ expect(response).to have_http_status(302)
+ expect(Rails.application.routes.recognize_path(response.redirect_url)).to include(
+ controller: 'projects/wikis',
+ action: 'show'
+ )
+ expect(response.redirect_url).to match(/\?random_title=true\Z/)
+ end
+ end
+
describe 'GET #pages' do
subject { get :pages, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } }
@@ -75,40 +90,62 @@ describe Projects::WikisController do
describe 'GET #show' do
render_views
- subject { get :show, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } }
+ let(:random_title) { nil }
- it 'limits the retrieved pages for the sidebar' do
- expect(controller).to receive(:load_wiki).and_return(project_wiki)
+ subject { get :show, params: { namespace_id: project.namespace, project_id: project, id: id, random_title: random_title } }
- # empty? call
- expect(project_wiki).to receive(:list_pages).with(limit: 1).and_call_original
- # Sidebar entries
- expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original
+ context 'when page exists' do
+ let(:id) { wiki_title }
- subject
+ it 'limits the retrieved pages for the sidebar' do
+ expect(controller).to receive(:load_wiki).and_return(project_wiki)
+ expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original
+
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:page).title).to eq(wiki_title)
+ end
+
+ context 'when page content encoding is invalid' do
+ it 'sets flash error' do
+ allow(controller).to receive(:valid_encoding?).and_return(false)
- expect(response).to have_http_status(:ok)
- expect(response.body).to include(wiki_title)
+ subject
+
+ expect(response).to have_http_status(:ok)
+ expect(flash[:notice]).to eq('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.')
+ end
+ end
end
- context 'when page content encoding is invalid' do
- it 'sets flash error' do
- allow(controller).to receive(:valid_encoding?).and_return(false)
+ context 'when the page does not exist' do
+ let(:id) { 'does not exist' }
+ before do
subject
+ end
- expect(response).to have_http_status(:ok)
- expect(flash[:notice]).to eq 'The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.'
+ it 'builds a new wiki page with the id as the title' do
+ expect(assigns(:page).title).to eq(id)
+ end
+
+ context 'when a random_title param is present' do
+ let(:random_title) { true }
+
+ it 'builds a new wiki page with no title' do
+ expect(assigns(:page).title).to be_empty
+ end
end
end
context 'when page is a file' do
include WikiHelpers
- let(:path) { upload_file_to_wiki(project, user, file_name) }
+ let(:id) { upload_file_to_wiki(project, user, file_name) }
before do
- get :show, params: { namespace_id: project.namespace, project_id: project, id: path }
+ subject
end
context 'when file is an image' do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index fed4fc810f2..35487682462 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -129,9 +129,9 @@ describe RegistrationsController do
{
message: auth_log_message,
env: :invisible_captcha_signup_bot_detected,
- ip: '0.0.0.0',
+ remote_ip: '0.0.0.0',
request_method: 'POST',
- fullpath: '/users'
+ path: '/users'
}
end
diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb
index 652533ac49f..fd4b95ce226 100644
--- a/spec/controllers/snippets/notes_controller_spec.rb
+++ b/spec/controllers/snippets/notes_controller_spec.rb
@@ -288,11 +288,13 @@ describe Snippets::NotesController do
describe 'POST toggle_award_emoji' do
let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) }
+ let(:emoji_name) { 'thumbsup'}
+
before do
sign_in(user)
end
- subject { post(:toggle_award_emoji, params: { snippet_id: public_snippet, id: note.id, name: "thumbsup" }) }
+ subject { post(:toggle_award_emoji, params: { snippet_id: public_snippet, id: note.id, name: emoji_name }) }
it "toggles the award emoji" do
expect { subject }.to change { note.award_emoji.count }.by(1)
@@ -301,7 +303,7 @@ describe Snippets::NotesController do
end
it "removes the already awarded emoji when it exists" do
- note.toggle_award_emoji('thumbsup', user) # create award emoji before
+ create(:award_emoji, awardable: note, name: emoji_name, user: user)
expect { subject }.to change { AwardEmoji.count }.by(-1)
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 8b8d4c57000..5566df0c216 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -19,7 +19,7 @@ describe UsersController do
it 'renders the show template' do
get :show, params: { username: user.username }
- expect(response).to be_success
+ expect(response).to be_successful
expect(response).to render_template('show')
end
end
@@ -362,7 +362,7 @@ describe UsersController do
it 'responds with success' do
get :show, params: { username: user.username }
- expect(response).to be_success
+ expect(response).to be_successful
end
end
@@ -418,7 +418,7 @@ describe UsersController do
it 'responds with success' do
get :projects, params: { username: user.username }
- expect(response).to be_success
+ expect(response).to be_successful
end
end
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 7b511c4d3d5..5c6b04a7141 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'Projects > Wiki > User previews markdown changes', :js do
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) }
let(:wiki_content) do
@@ -20,23 +20,12 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
project.add_maintainer(user)
sign_in(user)
-
- visit project_wiki_path(project, wiki_page)
end
context "while creating a new wiki page" do
context "when there are no spaces or hyphens in the page name" do
it "rewrites relative links as expected" do
- find('.add-new-wiki').click
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'a/b/c/d'
- click_button 'Create page'
- end
-
- page.within '.wiki-form' do
- fill_in :wiki_content, with: wiki_content
- click_on "Preview"
- end
+ create_wiki_page('a/b/c/d', content: wiki_content)
expect(page).to have_content("regular link")
@@ -50,16 +39,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
context "when there are spaces in the page name" do
it "rewrites relative links as expected" do
- click_link 'New page'
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'a page/b page/c page/d page'
- click_button 'Create page'
- end
-
- page.within '.wiki-form' do
- fill_in :wiki_content, with: wiki_content
- click_on "Preview"
- end
+ create_wiki_page('a page/b page/c page/d page', content: wiki_content)
expect(page).to have_content("regular link")
@@ -73,16 +53,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
context "when there are hyphens in the page name" do
it "rewrites relative links as expected" do
- click_link 'New page'
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'a-page/b-page/c-page/d-page'
- click_button 'Create page'
- end
-
- page.within '.wiki-form' do
- fill_in :wiki_content, with: wiki_content
- click_on "Preview"
- end
+ create_wiki_page('a-page/b-page/c-page/d-page', content: wiki_content)
expect(page).to have_content("regular link")
@@ -96,23 +67,9 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
end
context "while editing a wiki page" do
- def create_wiki_page(path)
- find('.add-new-wiki').click
-
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: path
- click_button 'Create page'
- end
-
- page.within '.wiki-form' do
- fill_in :wiki_content, with: 'content'
- click_on "Create page"
- end
- end
-
context "when there are no spaces or hyphens in the page name" do
it "rewrites relative links as expected" do
- create_wiki_page 'a/b/c/d'
+ create_wiki_page('a/b/c/d')
click_link 'Edit'
fill_in :wiki_content, with: wiki_content
@@ -130,7 +87,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
context "when there are spaces in the page name" do
it "rewrites relative links as expected" do
- create_wiki_page 'a page/b page/c page/d page'
+ create_wiki_page('a page/b page/c page/d page')
click_link 'Edit'
fill_in :wiki_content, with: wiki_content
@@ -148,7 +105,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
context "when there are hyphens in the page name" do
it "rewrites relative links as expected" do
- create_wiki_page 'a-page/b-page/c-page/d-page'
+ create_wiki_page('a-page/b-page/c-page/d-page')
click_link 'Edit'
fill_in :wiki_content, with: wiki_content
@@ -166,7 +123,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
context 'when rendering the preview' do
it 'renders content with CommonMark' do
- create_wiki_page 'a-page/b-page/c-page/common-mark'
+ create_wiki_page('a-page/b-page/c-page/common-mark')
click_link 'Edit'
fill_in :wiki_content, with: "1. one\n - sublist\n"
@@ -180,25 +137,31 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
end
it "does not linkify double brackets inside code blocks as expected" do
- click_link 'New page'
- page.within '#modal-new-wiki' do
- fill_in :new_wiki_path, with: 'linkify_test'
- click_button 'Create page'
- end
+ wiki_content = <<-HEREDOC
+ `[[do_not_linkify]]`
+ ```
+ [[also_do_not_linkify]]
+ ```
+ HEREDOC
- page.within '.wiki-form' do
- fill_in :wiki_content, with: <<-HEREDOC
- `[[do_not_linkify]]`
- ```
- [[also_do_not_linkify]]
- ```
- HEREDOC
- click_on "Preview"
- end
+ create_wiki_page('linkify_test', wiki_content)
expect(page).to have_content("do_not_linkify")
expect(page.html).to include('[[do_not_linkify]]')
expect(page.html).to include('[[also_do_not_linkify]]')
end
+
+ private
+
+ def create_wiki_page(path, content = 'content')
+ visit project_wiki_path(project, wiki_page)
+
+ click_link 'New page'
+
+ fill_in :wiki_title, with: path
+ fill_in :wiki_content, with: content
+
+ click_button 'Create page'
+ end
end
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index cc6dbaa6eb8..56d0518015d 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -42,10 +42,10 @@ describe "User creates wiki page" do
click_link("link test")
- expect(page).to have_content("Create Page")
+ expect(page).to have_content("Create New Page")
end
- it "shows non-escaped link in the pages list", :js, :quarantine do
+ it "shows non-escaped link in the pages list", :quarantine do
fill_in(:wiki_title, with: "one/two/three-test")
page.within(".wiki-form") do
@@ -58,7 +58,9 @@ describe "User creates wiki page" do
expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']")
end
- it "has `Create home` as a commit message" do
+ it "has `Create home` as a commit message", :js do
+ wait_for_requests
+
expect(page).to have_field("wiki[message]", with: "Create home")
end
@@ -81,7 +83,7 @@ describe "User creates wiki page" do
expect(current_path).to eq(project_wiki_path(project, "test"))
page.within(:css, ".nav-text") do
- expect(page).to have_content("test").and have_content("Create Page")
+ expect(page).to have_content("Create New Page")
end
click_link("Home")
@@ -93,7 +95,7 @@ describe "User creates wiki page" do
expect(current_path).to eq(project_wiki_path(project, "api"))
page.within(:css, ".nav-text") do
- expect(page).to have_content("Create").and have_content("api")
+ expect(page).to have_content("Create")
end
click_link("Home")
@@ -105,7 +107,7 @@ describe "User creates wiki page" do
expect(current_path).to eq(project_wiki_path(project, "raketasks"))
page.within(:css, ".nav-text") do
- expect(page).to have_content("Create").and have_content("rake")
+ expect(page).to have_content("Create")
end
end
@@ -150,6 +152,8 @@ describe "User creates wiki page" do
let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
it "has `Create home` as a commit message" do
+ wait_for_requests
+
expect(page).to have_field("wiki[message]", with: "Create home")
end
@@ -181,20 +185,15 @@ describe "User creates wiki page" do
it "creates a page with a single word" do
click_link("New page")
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "foo")
-
- click_button("Create page")
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "foo")
+ fill_in(:wiki_content, with: "My awesome wiki!")
end
# Commit message field should have correct value.
expect(page).to have_field("wiki[message]", with: "Create foo")
- page.within(".wiki-form") do
- fill_in(:wiki_content, with: "My awesome wiki!")
-
- click_button("Create page")
- end
+ click_button("Create page")
expect(page).to have_content("foo")
.and have_content("Last edited by #{user.name}")
@@ -204,20 +203,15 @@ describe "User creates wiki page" do
it "creates a page with spaces in the name" do
click_link("New page")
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "Spaces in the name")
-
- click_button("Create page")
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "Spaces in the name")
+ fill_in(:wiki_content, with: "My awesome wiki!")
end
# Commit message field should have correct value.
expect(page).to have_field("wiki[message]", with: "Create Spaces in the name")
- page.within(".wiki-form") do
- fill_in(:wiki_content, with: "My awesome wiki!")
-
- click_button("Create page")
- end
+ click_button("Create page")
expect(page).to have_content("Spaces in the name")
.and have_content("Last edited by #{user.name}")
@@ -227,10 +221,9 @@ describe "User creates wiki page" do
it "creates a page with hyphens in the name" do
click_link("New page")
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "hyphens-in-the-name")
-
- click_button("Create page")
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "hyphens-in-the-name")
+ fill_in(:wiki_content, with: "My awesome wiki!")
end
# Commit message field should have correct value.
@@ -251,12 +244,6 @@ describe "User creates wiki page" do
it "shows the emoji autocompletion dropdown" do
click_link("New page")
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "test-autocomplete")
-
- click_button("Create page")
- end
-
page.within(".wiki-form") do
find("#wiki_content").native.send_keys("")
@@ -274,20 +261,15 @@ describe "User creates wiki page" do
it "creates a page" do
click_link("New page")
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "foo")
-
- click_button("Create page")
+ page.within(".wiki-form") do
+ fill_in(:wiki_title, with: "foo")
+ fill_in(:wiki_content, with: "My awesome wiki!")
end
# Commit message field should have correct value.
expect(page).to have_field("wiki[message]", with: "Create foo")
- page.within(".wiki-form") do
- fill_in(:wiki_content, with: "My awesome wiki!")
-
- click_button("Create page")
- end
+ click_button("Create page")
expect(page).to have_content("foo")
.and have_content("Last edited by #{user.name}")
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index 2aab8fda62d..3f3711f9eb8 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -70,7 +70,7 @@ describe 'User updates wiki page' do
context 'in a user namespace' do
let(:project) { create(:project, :wiki_repo) }
- it 'updates a page' do
+ it 'updates a page', :js do
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home')
@@ -82,6 +82,18 @@ describe 'User updates wiki page' do
expect(page).to have_content('My awesome wiki!')
end
+ it 'updates the commit message as the title is changed', :js do
+ fill_in(:wiki_title, with: 'Wiki title')
+
+ expect(page).to have_field('wiki[message]', with: 'Update Wiki title')
+ end
+
+ it 'does not allow XSS', :js do
+ fill_in(:wiki_title, with: '<script>')
+
+ expect(page).to have_field('wiki[message]', with: 'Update &lt;script&gt;')
+ end
+
it 'shows a validation error message' do
fill_in(:wiki_content, with: '')
click_button('Save changes')
@@ -129,7 +141,7 @@ describe 'User updates wiki page' do
context 'in a group namespace' do
let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) }
- it 'updates a page' do
+ it 'updates a page', :js do
# Commit message field should have correct value.
expect(page).to have_field('wiki[message]', with: 'Update home')
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 05742b63c43..77e725e7f11 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -101,8 +101,7 @@ describe 'User views a wiki page' do
click_on('image')
expect(current_path).to match("wikis/#{path}")
- expect(page).to have_content('New Wiki Page')
- expect(page).to have_content('Create page')
+ expect(page).to have_content('Create New Page')
end
end
@@ -156,6 +155,6 @@ describe 'User views a wiki page' do
find('.shortcuts-wiki').click
click_link "Create your first page"
- expect(page).to have_content('Home · Create Page')
+ expect(page).to have_content('Create New Page')
end
end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 5a60991c1bf..4ca79ccaea8 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -39,17 +39,16 @@ describe 'User searches for code' do
context 'when on a project page', :js do
before do
visit(search_path)
- end
-
- include_examples 'top right search form'
-
- it 'finds code' do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
click_link(project.full_name)
end
+ end
+ include_examples 'top right search form'
+
+ it 'finds code' do
fill_in('dashboard_search', with: 'rspec')
find('.btn-search').click
@@ -57,6 +56,27 @@ describe 'User searches for code' do
expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions')
end
end
+
+ it 'search mutiple words with refs switching' do
+ expected_result = 'Use `snake_case` for naming files'
+ search = 'for naming files'
+
+ fill_in('dashboard_search', with: search)
+ find('.btn-search').click
+
+ page.within('.results') do
+ expect(find('.search-results')).to have_content(expected_result)
+ end
+
+ find('.js-project-refs-dropdown').click
+ find('.dropdown-page-one .dropdown-content').click_link('v1.0.0')
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_content(expected_result)
+ end
+
+ expect(find_field('dashboard_search').value).to eq(search)
+ end
end
context 'search code within refs', :js do
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index e2b3444272e..70e6978a7b6 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -15,8 +15,8 @@ describe 'GPG signed commits' do
visit project_commit_path(project, ref)
- expect(page).to have_link 'Unverified'
- expect(page).not_to have_link 'Verified'
+ expect(page).to have_button 'Unverified'
+ expect(page).not_to have_button 'Verified'
# user changes his email which makes the gpg key verified
perform_enqueued_jobs do
@@ -26,8 +26,8 @@ describe 'GPG signed commits' do
visit project_commit_path(project, ref)
- expect(page).not_to have_link 'Unverified'
- expect(page).to have_link 'Verified'
+ expect(page).not_to have_button 'Unverified'
+ expect(page).to have_button 'Verified'
end
it 'changes from unverified to verified when the user adds the missing gpg key' do
@@ -36,8 +36,8 @@ describe 'GPG signed commits' do
visit project_commit_path(project, ref)
- expect(page).to have_link 'Unverified'
- expect(page).not_to have_link 'Verified'
+ expect(page).to have_button 'Unverified'
+ expect(page).not_to have_button 'Verified'
# user adds the gpg key which makes the signature valid
perform_enqueued_jobs do
@@ -46,8 +46,8 @@ describe 'GPG signed commits' do
visit project_commit_path(project, ref)
- expect(page).not_to have_link 'Unverified'
- expect(page).to have_link 'Verified'
+ expect(page).not_to have_button 'Unverified'
+ expect(page).to have_button 'Verified'
end
context 'shows popover badges', :js do
@@ -136,7 +136,7 @@ describe 'GPG signed commits' do
visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA)
# wait for the signature to get generated
- expect(page).to have_link 'Verified'
+ expect(page).to have_button 'Verified'
user_1.destroy!
diff --git a/spec/finders/award_emojis_finder_spec.rb b/spec/finders/award_emojis_finder_spec.rb
new file mode 100644
index 00000000000..ccac475daad
--- /dev/null
+++ b/spec/finders/award_emojis_finder_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardEmojisFinder do
+ set(:issue_1) { create(:issue) }
+ set(:issue_1_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_1) }
+ set(:issue_1_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_1) }
+ # Create a matching set of emoji for a second issue.
+ # These should never appear in our finder results
+ set(:issue_2) { create(:issue) }
+ set(:issue_2_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_2) }
+ set(:issue_2_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_2) }
+
+ describe 'param validation' do
+ it 'raises an error if `name` is invalid' do
+ expect { described_class.new(issue_1, { name: 'invalid' }).execute }.to raise_error(
+ ArgumentError,
+ 'Invalid name param'
+ )
+ end
+
+ it 'raises an error if `awarded_by` is invalid' do
+ expectation = [ArgumentError, 'Invalid awarded_by param']
+
+ expect { described_class.new(issue_1, { awarded_by: issue_2 }).execute }.to raise_error(*expectation)
+ expect { described_class.new(issue_1, { awarded_by: 'not-an-id' }).execute }.to raise_error(*expectation)
+ expect { described_class.new(issue_1, { awarded_by: 1.123 }).execute }.to raise_error(*expectation)
+ end
+ end
+
+ describe '#execute' do
+ it 'scopes to the awardable' do
+ expect(described_class.new(issue_1).execute).to contain_exactly(
+ issue_1_thumbsup, issue_1_thumbsdown
+ )
+ end
+
+ it 'filters by emoji name' do
+ expect(described_class.new(issue_1, { name: 'thumbsup' }).execute).to contain_exactly(issue_1_thumbsup)
+ expect(described_class.new(issue_1, { name: '8ball' }).execute).to be_empty
+ end
+
+ it 'filters by user' do
+ expect(described_class.new(issue_1, { awarded_by: issue_1_thumbsup.user }).execute).to contain_exactly(issue_1_thumbsup)
+ expect(described_class.new(issue_1, { awarded_by: issue_2_thumbsup.user }).execute).to be_empty
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json
index 9216ad0060b..fe725b97c21 100644
--- a/spec/fixtures/api/schemas/deployment.json
+++ b/spec/fixtures/api/schemas/deployment.json
@@ -3,7 +3,7 @@
"required": [
"sha",
"created_at",
- "finished_at",
+ "deployed_at",
"iid",
"tag",
"last?",
@@ -12,7 +12,7 @@
],
"properties": {
"created_at": { "type": "string" },
- "finished_at": { "type": ["string", "null"] },
+ "deployed_at": { "type": ["string", "null"] },
"id": { "type": "integer" },
"iid": { "type": "integer" },
"last?": { "type": "boolean" },
diff --git a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
index 214b67a9a0f..9945de8a856 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
@@ -2,6 +2,7 @@
"type": "object",
"properties" : {
"id": { "type": "integer" },
+ "iid": { "type": "integer" },
"type": { "type": "string" },
"author_id": { "type": "integer" },
"project_id": { "type": "integer" },
diff --git a/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json
deleted file mode 100644
index 8fb66f6652b..00000000000
--- a/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json
+++ /dev/null
@@ -1,422 +0,0 @@
-{
- "version": "2.1",
- "vulnerabilities": [
- {
- "category": "dependency_scanning",
- "name": "Vulnerabilities in libxml2",
- "message": "Vulnerabilities in libxml2 in nokogiri",
- "description": " The version of libxml2 packaged with Nokogiri contains several vulnerabilities.\r\n Nokogiri has mitigated these issues by upgrading to libxml 2.9.5.\r\n\r\n It was discovered that a type confusion error existed in libxml2. An\r\n attacker could use this to specially construct XML data that\r\n could cause a denial of service or possibly execute arbitrary\r\n code. (CVE-2017-0663)\r\n\r\n It was discovered that libxml2 did not properly validate parsed entity\r\n references. An attacker could use this to specially construct XML\r\n data that could expose sensitive information. (CVE-2017-7375)\r\n\r\n It was discovered that a buffer overflow existed in libxml2 when\r\n handling HTTP redirects. An attacker could use this to specially\r\n construct XML data that could cause a denial of service or possibly\r\n execute arbitrary code. (CVE-2017-7376)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered a buffer overflow in\r\n libxml2 when handling elements. An attacker could use this to specially\r\n construct XML data that could cause a denial of service or possibly\r\n execute arbitrary code. (CVE-2017-9047)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered a buffer overread\r\n in libxml2 when handling elements. An attacker could use this\r\n to specially construct XML data that could cause a denial of\r\n service. (CVE-2017-9048)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered multiple buffer overreads\r\n in libxml2 when handling parameter-entity references. An attacker\r\n could use these to specially construct XML data that could cause a\r\n denial of service. (CVE-2017-9049, CVE-2017-9050)",
- "cve": "rails/Gemfile.lock:nokogiri:gemnasium:06565b64-486d-4326-b906-890d9915804d",
- "severity": "High",
- "solution": "Upgrade to latest version.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "rails/Gemfile.lock",
- "dependency": {
- "package": {
- "name": "nokogiri"
- },
- "version": "1.8.0"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d",
- "value": "06565b64-486d-4326-b906-890d9915804d",
- "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
- },
- {
- "type": "usn",
- "name": "USN-3424-1",
- "value": "USN-3424-1",
- "url": "https://usn.ubuntu.com/3424-1/"
- }
- ],
- "links": [
- {
- "url": "https://github.com/sparklemotion/nokogiri/issues/1673"
- }
- ]
- },
- {
- "category": "dependency_scanning",
- "name": "Infinite recursion in parameter entities",
- "message": "Infinite recursion in parameter entities in nokogiri",
- "description": "libxml2 incorrectly handles certain parameter entities. An attacker can leverage this with specially constructed XML data to cause libxml2 to consume resources, leading to a denial of service.",
- "cve": "rails/Gemfile.lock:nokogiri:gemnasium:6a0d56f6-2441-492a-9b14-edb95ac31919",
- "severity": "High",
- "solution": "Upgrade to latest version.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "rails/Gemfile.lock",
- "dependency": {
- "package": {
- "name": "nokogiri"
- },
- "version": "1.8.0"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-6a0d56f6-2441-492a-9b14-edb95ac31919",
- "value": "6a0d56f6-2441-492a-9b14-edb95ac31919",
- "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2017-16932",
- "value": "CVE-2017-16932",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16932"
- }
- ],
- "links": [
- {
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16932"
- },
- {
- "url": "https://github.com/sparklemotion/nokogiri/issues/1714"
- },
- {
- "url": "https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-16932.html"
- },
- {
- "url": "https://usn.ubuntu.com/usn/usn-3504-1/"
- }
- ]
- },
- {
- "category": "dependency_scanning",
- "name": "Denial of Service",
- "message": "Denial of Service in nokogiri",
- "description": "libxml2 incorrectly handles certain files. An attacker can use this issue with specially constructed XML data to cause libxml2 to consume resources, leading to a denial of service.\r\n\r\n",
- "cve": "rails/Gemfile.lock:nokogiri:gemnasium:78658378-bd8f-4d79-81c8-07c419302426",
- "severity": "Unknown",
- "solution": "Upgrade to latest version.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "rails/Gemfile.lock",
- "dependency": {
- "package": {
- "name": "nokogiri"
- },
- "version": "1.8.0"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-78658378-bd8f-4d79-81c8-07c419302426",
- "value": "78658378-bd8f-4d79-81c8-07c419302426",
- "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2017-15412",
- "value": "CVE-2017-15412",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15412"
- }
- ],
- "links": [
- {
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15412"
- },
- {
- "url": "https://github.com/sparklemotion/nokogiri/issues/1714"
- },
- {
- "url": "https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-15412.html"
- }
- ]
- },
- {
- "category": "dependency_scanning",
- "name": "Bypass of a protection mechanism in libxslt",
- "message": "Bypass of a protection mechanism in libxslt in nokogiri",
- "description": "libxslt through 1.1.33 allows bypass of a protection mechanism because callers of xsltCheckRead and xsltCheckWrite permit access even upon receiving a -1 error code. xsltCheckRead can return -1 for a crafted URL that is not actually invalid and is subsequently loaded. Vendored version of libxslt has been patched to remediate this vulnerability. Note that this patch is not yet (as of 2019-04-22) in an upstream release of libxslt.",
- "cve": "rails/Gemfile.lock:nokogiri:gemnasium:1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
- "severity": "Unknown",
- "solution": "Upgrade to latest version if using vendored version of libxslt OR update the system library libxslt to a fixed version",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "rails/Gemfile.lock",
- "dependency": {
- "package": {
- "name": "nokogiri"
- },
- "version": "1.8.0"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
- "value": "1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
- "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2019-11068",
- "value": "CVE-2019-11068",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11068"
- }
- ],
- "links": [
- {
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11068"
- },
- {
- "url": "https://github.com/sparklemotion/nokogiri/issues/1892"
- },
- {
- "url": "https://people.canonical.com/~ubuntu-security/cve/CVE-2019-11068"
- },
- {
- "url": "https://security-tracker.debian.org/tracker/CVE-2019-11068"
- }
- ]
- },
- {
- "category": "dependency_scanning",
- "name": "Regular Expression Denial of Service",
- "message": "Regular Expression Denial of Service in debug",
- "description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.",
- "cve": "yarn/yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a",
- "severity": "Unknown",
- "solution": "Upgrade to latest versions.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "yarn/yarn.lock",
- "dependency": {
- "package": {
- "name": "debug"
- },
- "version": "1.0.5"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a",
- "value": "37283ed4-0380-40d7-ada7-2d994afcc62a",
- "url": "https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories"
- }
- ],
- "links": [
- {
- "url": "https://github.com/visionmedia/debug/issues/501"
- },
- {
- "url": "https://github.com/visionmedia/debug/pull/504"
- },
- {
- "url": "https://nodesecurity.io/advisories/534"
- }
- ]
- },
- {
- "category": "dependency_scanning",
- "name": "Authentication bypass via incorrect DOM traversal and canonicalization",
- "message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
- "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
- "cve": "yarn/yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98",
- "severity": "Unknown",
- "solution": "Upgrade to fixed version.\r\n",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "yarn/yarn.lock",
- "dependency": {
- "package": {
- "name": "saml2-js"
- },
- "version": "1.5.0"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98",
- "value": "9952e574-7b5b-46fa-a270-aeb694198a98",
- "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2017-11429",
- "value": "CVE-2017-11429",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429"
- }
- ],
- "links": [
- {
- "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279"
- },
- {
- "url": "https://github.com/Clever/saml2/issues/127"
- },
- {
- "url": "https://www.kb.cert.org/vuls/id/475445"
- }
- ]
- }
- ],
- "remediations": [],
- "dependency_files": [
- {
- "path": "rails/Gemfile.lock",
- "package_manager": "bundler",
- "dependencies": [
- {
- "package": {
- "name": "mini_portile2"
- },
- "version": "2.2.0"
- },
- {
- "package": {
- "name": "nokogiri"
- },
- "version": "1.8.0"
- }
- ]
- },
- {
- "path": "yarn/yarn.lock",
- "package_manager": "yarn",
- "dependencies": [
- {
- "package": {
- "name": "async"
- },
- "version": "0.2.10"
- },
- {
- "package": {
- "name": "async"
- },
- "version": "1.5.2"
- },
- {
- "package": {
- "name": "debug"
- },
- "version": "1.0.5"
- },
- {
- "package": {
- "name": "ejs"
- },
- "version": "0.8.8"
- },
- {
- "package": {
- "name": "ms"
- },
- "version": "2.0.0"
- },
- {
- "package": {
- "name": "node-forge"
- },
- "version": "0.2.24"
- },
- {
- "package": {
- "name": "saml2-js"
- },
- "version": "1.5.0"
- },
- {
- "package": {
- "name": "sax"
- },
- "version": "1.2.4"
- },
- {
- "package": {
- "name": "underscore"
- },
- "version": "1.9.1"
- },
- {
- "package": {
- "name": "underscore"
- },
- "version": "1.6.0"
- },
- {
- "package": {
- "name": "xml-crypto"
- },
- "version": "0.8.5"
- },
- {
- "package": {
- "name": "xml-encryption"
- },
- "version": "0.7.4"
- },
- {
- "package": {
- "name": "xml2js"
- },
- "version": "0.4.19"
- },
- {
- "package": {
- "name": "xmlbuilder"
- },
- "version": "2.1.0"
- },
- {
- "package": {
- "name": "xmlbuilder"
- },
- "version": "9.0.7"
- },
- {
- "package": {
- "name": "xmldom"
- },
- "version": "0.1.19"
- },
- {
- "package": {
- "name": "xmldom"
- },
- "version": "0.1.27"
- },
- {
- "package": {
- "name": "xpath.js"
- },
- "version": "1.1.0"
- },
- {
- "package": {
- "name": "xpath"
- },
- "version": "0.0.5"
- }
- ]
- }
- ]
-}
diff --git a/spec/fixtures/security-reports/deprecated/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/deprecated/gl-dependency-scanning-report.json
deleted file mode 100644
index ce66f562175..00000000000
--- a/spec/fixtures/security-reports/deprecated/gl-dependency-scanning-report.json
+++ /dev/null
@@ -1,178 +0,0 @@
-[
- {
- "category": "dependency_scanning",
- "name": "io.netty/netty - CVE-2014-3488",
- "message": "DoS by CPU exhaustion when using malicious SSL packets",
- "cve": "app/pom.xml:io.netty/netty@3.9.1.Final:CVE-2014-3488",
- "severity": "Unknown",
- "solution": "Upgrade to the latest version",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "app/pom.xml",
- "dependency": {
- "package": {
- "name": "io.netty/netty"
- },
- "version": "3.9.1.Final"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-d1bf36d9-9f07-46cd-9cfc-8675338ada8f",
- "value": "d1bf36d9-9f07-46cd-9cfc-8675338ada8f",
- "url": "https://deps.sec.gitlab.com/packages/maven/io.netty/netty/versions/3.9.1.Final/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2014-3488",
- "value": "CVE-2014-3488",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3488"
- }
- ],
- "links": [
- {
- "url": "https://bugzilla.redhat.com/CVE-2014-3488"
- },
- {
- "url": "http://netty.io/news/2014/06/11/3.html"
- },
- {
- "url": "https://github.com/netty/netty/issues/2562"
- }
- ],
- "priority": "Unknown",
- "file": "app/pom.xml",
- "url": "https://bugzilla.redhat.com/CVE-2014-3488",
- "tool": "gemnasium"
- },
- {
- "category": "dependency_scanning",
- "name": "Django - CVE-2017-12794",
- "message": "Possible XSS in traceback section of technical 500 debug page",
- "cve": "app/requirements.txt:Django@1.11.3:CVE-2017-12794",
- "severity": "Unknown",
- "solution": "Upgrade to latest version or apply patch.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "app/requirements.txt",
- "dependency": {
- "package": {
- "name": "Django"
- },
- "version": "1.11.3"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-6162a015-8635-4a15-8d7c-dc9321db366f",
- "value": "6162a015-8635-4a15-8d7c-dc9321db366f",
- "url": "https://deps.sec.gitlab.com/packages/pypi/Django/versions/1.11.3/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2017-12794",
- "value": "CVE-2017-12794",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12794"
- }
- ],
- "links": [
- {
- "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/"
- }
- ],
- "priority": "Unknown",
- "file": "app/requirements.txt",
- "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/",
- "tool": "gemnasium"
- },
- {
- "category": "dependency_scanning",
- "name": "nokogiri - USN-3424-1",
- "message": "Vulnerabilities in libxml2",
- "cve": "rails/Gemfile.lock:nokogiri@1.8.0:USN-3424-1",
- "severity": "Unknown",
- "solution": "Upgrade to latest version.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "rails/Gemfile.lock",
- "dependency": {
- "package": {
- "name": "nokogiri"
- },
- "version": "1.8.0"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d",
- "value": "06565b64-486d-4326-b906-890d9915804d",
- "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
- },
- {
- "type": "usn",
- "name": "USN-3424-1",
- "value": "USN-3424-1",
- "url": "https://usn.ubuntu.com/3424-1/"
- }
- ],
- "links": [
- {
- "url": "https://github.com/sparklemotion/nokogiri/issues/1673"
- }
- ],
- "priority": "Unknown",
- "file": "rails/Gemfile.lock",
- "url": "https://github.com/sparklemotion/nokogiri/issues/1673",
- "tool": "gemnasium"
- },
- {
- "category": "dependency_scanning",
- "name": "ffi - CVE-2018-1000201",
- "message": "ruby-ffi DDL loading issue on Windows OS",
- "cve": "ffi:1.9.18:CVE-2018-1000201",
- "severity": "High",
- "solution": "upgrade to \u003e= 1.9.24",
- "scanner": {
- "id": "bundler_audit",
- "name": "bundler-audit"
- },
- "location": {
- "file": "sast-sample-rails/Gemfile.lock",
- "dependency": {
- "package": {
- "name": "ffi"
- },
- "version": "1.9.18"
- }
- },
- "identifiers": [
- {
- "type": "cve",
- "name": "CVE-2018-1000201",
- "value": "CVE-2018-1000201",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201"
- }
- ],
- "links": [
- {
- "url": "https://github.com/ffi/ffi/releases/tag/1.9.24"
- }
- ],
- "priority": "High",
- "file": "sast-sample-rails/Gemfile.lock",
- "url": "https://github.com/ffi/ffi/releases/tag/1.9.24",
- "tool": "bundler_audit"
- }
-]
diff --git a/spec/fixtures/security-reports/deprecated/gl-sast-report.json b/spec/fixtures/security-reports/deprecated/gl-sast-report.json
deleted file mode 100644
index 2f7e47281e2..00000000000
--- a/spec/fixtures/security-reports/deprecated/gl-sast-report.json
+++ /dev/null
@@ -1,964 +0,0 @@
-[
- {
- "category": "sast",
- "message": "Probable insecure usage of temp file/directory.",
- "cve": "python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-tmp.py",
- "start_line": 1,
- "end_line": 1
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B108",
- "value": "B108",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
- }
- ],
- "priority": "Medium",
- "file": "python/hardcoded/hardcoded-tmp.py",
- "line": 1,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "name": "Predictable pseudorandom number generator",
- "message": "Predictable pseudorandom number generator",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 47,
- "end_line": 47,
- "class": "com.gitlab.security_products.tests.App",
- "method": "generateSecretToken2"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-PREDICTABLE_RANDOM",
- "value": "PREDICTABLE_RANDOM",
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 47,
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "name": "Predictable pseudorandom number generator",
- "message": "Predictable pseudorandom number generator",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 41,
- "end_line": 41,
- "class": "com.gitlab.security_products.tests.App",
- "method": "generateSecretToken1"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-PREDICTABLE_RANDOM",
- "value": "PREDICTABLE_RANDOM",
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 41,
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:cb203b465dffb0cb3a8e8bd8910b84b93b0a5995a938e4b903dbb0cd6ffa1254:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 11,
- "end_line": 11
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 11,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:a7173c43ae66bd07466632d819d450e0071e02dbf782763640d1092981f9631b:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 12,
- "end_line": 12
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 12,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:017017b77deb0b8369b6065947833eeea752a92ec8a700db590fece3e934cf0d:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 13,
- "end_line": 13
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 13,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:45fc8c53aea7b84f06bc4e590cc667678d6073c4c8a1d471177ca2146fb22db2:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 14,
- "end_line": 14
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 14,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Pickle library appears to be in use, possible security issue.",
- "cve": "python/imports/imports-aliases.py:5f200d47291e7bbd8352db23019b85453ca048dd98ea0c291260fa7d009963a4:B301",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 15,
- "end_line": 15
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B301",
- "value": "B301"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 15,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "name": "ECB mode is insecure",
- "message": "ECB mode is insecure",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:ECB_MODE",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 29,
- "end_line": 29,
- "class": "com.gitlab.security_products.tests.App",
- "method": "insecureCypher"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-ECB_MODE",
- "value": "ECB_MODE",
- "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 29,
- "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "name": "Cipher with no integrity",
- "message": "Cipher with no integrity",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:CIPHER_INTEGRITY",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 29,
- "end_line": 29,
- "class": "com.gitlab.security_products.tests.App",
- "method": "insecureCypher"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-CIPHER_INTEGRITY",
- "value": "CIPHER_INTEGRITY",
- "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 29,
- "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "message": "Probable insecure usage of temp file/directory.",
- "cve": "python/hardcoded/hardcoded-tmp.py:63dd4d626855555b816985d82c4614a790462a0a3ada89dc58eb97f9c50f3077:B108",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-tmp.py",
- "start_line": 14,
- "end_line": 14
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B108",
- "value": "B108",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
- }
- ],
- "priority": "Medium",
- "file": "python/hardcoded/hardcoded-tmp.py",
- "line": 14,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Probable insecure usage of temp file/directory.",
- "cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-tmp.py",
- "start_line": 10,
- "end_line": 10
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B108",
- "value": "B108",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
- }
- ],
- "priority": "Medium",
- "file": "python/hardcoded/hardcoded-tmp.py",
- "line": 10,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with Popen module.",
- "cve": "python/imports/imports-aliases.py:2c3e1fa1e54c3c6646e8bcfaee2518153c6799b77587ff8d9a7b0631f6d34785:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 1,
- "end_line": 1
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 1,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with pickle module.",
- "cve": "python/imports/imports.py:af58d07f6ad519ef5287fcae65bf1a6999448a1a3a8bc1ac2a11daa80d0b96bf:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports.py",
- "start_line": 2,
- "end_line": 2
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports.py",
- "line": 2,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with subprocess module.",
- "cve": "python/imports/imports.py:8de9bc98029d212db530785a5f6780cfa663548746ff228ab8fa96c5bb82f089:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports.py",
- "start_line": 4,
- "end_line": 4
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports.py",
- "line": 4,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'blerg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:97c30f1d76d2a88913e3ce9ae74087874d740f87de8af697a9c455f01119f633:B106",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 22,
- "end_line": 22
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B106",
- "value": "B106",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 22,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'root'",
- "cve": "python/hardcoded/hardcoded-passwords.py:7431c73a0bc16d94ece2a2e75ef38f302574d42c37ac0c3c38ad0b3bf8a59f10:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 5,
- "end_line": 5
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 5,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: ''",
- "cve": "python/hardcoded/hardcoded-passwords.py:d2d1857c27caedd49c57bfbcdc23afcc92bd66a22701fcdc632869aab4ca73ee:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 9,
- "end_line": 9
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 9,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'ajklawejrkl42348swfgkg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:fb3866215a61393a5c9c32a3b60e2058171a23219c353f722cbd3567acab21d2:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 13,
- "end_line": 13
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 13,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'blerg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:63c62a8b7e1e5224439bd26b28030585ac48741e28ca64561a6071080c560a5f:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 23,
- "end_line": 23
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 23,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'blerg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:4311b06d08df8fa58229b341c531da8e1a31ec4520597bdff920cd5c098d86f9:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 24,
- "end_line": 24
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 24,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with subprocess module.",
- "cve": "python/imports/imports-function.py:5858400c2f39047787702de44d03361ef8d954c9d14bd54ee1c2bef9e6a7df93:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-function.py",
- "start_line": 4,
- "end_line": 4
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-function.py",
- "line": 4,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with pickle module.",
- "cve": "python/imports/imports-function.py:dbda3cf4190279d30e0aad7dd137eca11272b0b225e8af4e8bf39682da67d956:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-function.py",
- "start_line": 2,
- "end_line": 2
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-function.py",
- "line": 2,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with Popen module.",
- "cve": "python/imports/imports-from.py:eb8a0db9cd1a8c1ab39a77e6025021b1261cc2a0b026b2f4a11fca4e0636d8dd:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-from.py",
- "start_line": 7,
- "end_line": 7
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-from.py",
- "line": 7,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell",
- "cve": "python/imports/imports-aliases.py:f99f9721e27537fbcb6699a4cf39c6740d6234d2c6f06cfc2d9ea977313c483d:B602",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 9,
- "end_line": 9
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B602",
- "value": "B602",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 9,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with subprocess module.",
- "cve": "python/imports/imports-from.py:332a12ab1146698f614a905ce6a6a5401497a12281aef200e80522711c69dcf4:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-from.py",
- "start_line": 6,
- "end_line": 6
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-from.py",
- "line": 6,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with Popen module.",
- "cve": "python/imports/imports-from.py:0a48de4a3d5348853a03666cb574697e3982998355e7a095a798bd02a5947276:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-from.py",
- "start_line": 1,
- "end_line": 2
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-from.py",
- "line": 1,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with pickle module.",
- "cve": "python/imports/imports-aliases.py:51b71661dff994bde3529639a727a678c8f5c4c96f00d300913f6d5be1bbdf26:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 7,
- "end_line": 8
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 7,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with loads module.",
- "cve": "python/imports/imports-aliases.py:6ff02aeb3149c01ab68484d794a94f58d5d3e3bb0d58557ef4153644ea68ea54:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 6,
- "end_line": 6
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 6,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)",
- "cve": "c/subdir/utils.c:b466873101951fe96e1332f6728eb7010acbbd5dfc3b65d7d53571d091a06d9e:CWE-119!/CWE-120",
- "confidence": "Low",
- "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "c/subdir/utils.c",
- "start_line": 4
- },
- "identifiers": [
- {
- "type": "flawfinder_func_name",
- "name": "Flawfinder - char",
- "value": "char"
- },
- {
- "type": "cwe",
- "name": "CWE-119",
- "value": "119",
- "url": "https://cwe.mitre.org/data/definitions/119.html"
- },
- {
- "type": "cwe",
- "name": "CWE-120",
- "value": "120",
- "url": "https://cwe.mitre.org/data/definitions/120.html"
- }
- ],
- "file": "c/subdir/utils.c",
- "line": 4,
- "url": "https://cwe.mitre.org/data/definitions/119.html",
- "tool": "flawfinder"
- },
- {
- "category": "sast",
- "message": "Check when opening files - can an attacker redirect it (via symlinks), force the opening of special file type (e.g., device files), move things around to create a race condition, control its ancestors, or change its contents? (CWE-362)",
- "cve": "c/subdir/utils.c:bab681140fcc8fc3085b6bba74081b44ea145c1c98b5e70cf19ace2417d30770:CWE-362",
- "confidence": "Low",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "c/subdir/utils.c",
- "start_line": 8
- },
- "identifiers": [
- {
- "type": "flawfinder_func_name",
- "name": "Flawfinder - fopen",
- "value": "fopen"
- },
- {
- "type": "cwe",
- "name": "CWE-362",
- "value": "362",
- "url": "https://cwe.mitre.org/data/definitions/362.html"
- }
- ],
- "file": "c/subdir/utils.c",
- "line": 8,
- "url": "https://cwe.mitre.org/data/definitions/362.html",
- "tool": "flawfinder"
- },
- {
- "category": "sast",
- "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)",
- "cve": "cplusplus/src/hello.cpp:c8c6dd0afdae6814194cf0930b719f757ab7b379cf8f261e7f4f9f2f323a818a:CWE-119!/CWE-120",
- "confidence": "Low",
- "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "cplusplus/src/hello.cpp",
- "start_line": 6
- },
- "identifiers": [
- {
- "type": "flawfinder_func_name",
- "name": "Flawfinder - char",
- "value": "char"
- },
- {
- "type": "cwe",
- "name": "CWE-119",
- "value": "119",
- "url": "https://cwe.mitre.org/data/definitions/119.html"
- },
- {
- "type": "cwe",
- "name": "CWE-120",
- "value": "120",
- "url": "https://cwe.mitre.org/data/definitions/120.html"
- }
- ],
- "file": "cplusplus/src/hello.cpp",
- "line": 6,
- "url": "https://cwe.mitre.org/data/definitions/119.html",
- "tool": "flawfinder"
- },
- {
- "category": "sast",
- "message": "Does not check for buffer overflows when copying to destination [MS-banned] (CWE-120)",
- "cve": "cplusplus/src/hello.cpp:331c04062c4fe0c7c486f66f59e82ad146ab33cdd76ae757ca41f392d568cbd0:CWE-120",
- "confidence": "Low",
- "solution": "Consider using snprintf, strcpy_s, or strlcpy (warning: strncpy easily misused)",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "cplusplus/src/hello.cpp",
- "start_line": 7
- },
- "identifiers": [
- {
- "type": "flawfinder_func_name",
- "name": "Flawfinder - strcpy",
- "value": "strcpy"
- },
- {
- "type": "cwe",
- "name": "CWE-120",
- "value": "120",
- "url": "https://cwe.mitre.org/data/definitions/120.html"
- }
- ],
- "file": "cplusplus/src/hello.cpp",
- "line": 7,
- "url": "https://cwe.mitre.org/data/definitions/120.html",
- "tool": "flawfinder"
- }
-]
diff --git a/spec/fixtures/security-reports/feature-branch.zip b/spec/fixtures/security-reports/feature-branch.zip
deleted file mode 100644
index dd49f4e9e1d..00000000000
--- a/spec/fixtures/security-reports/feature-branch.zip
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json b/spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json
deleted file mode 100644
index 6f89d20d4bf..00000000000
--- a/spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "image": "registry.gitlab.com/bikebilly/auto-devops-10-6/feature-branch:e7315ba964febb11bac8f5cd6ec433db8a3a1583",
- "unapproved": ["CVE-2017-15650"],
- "vulnerabilities": [
- {
- "featurename": "musl",
- "featureversion": "1.1.14-r15",
- "vulnerability": "CVE-2017-15650",
- "namespace": "alpine:v3.4",
- "description": "",
- "link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650",
- "severity": "Medium",
- "fixedby": "1.1.14-r16"
- }
- ]
-}
diff --git a/spec/fixtures/security-reports/feature-branch/gl-dast-report.json b/spec/fixtures/security-reports/feature-branch/gl-dast-report.json
deleted file mode 100644
index 3a308bf047e..00000000000
--- a/spec/fixtures/security-reports/feature-branch/gl-dast-report.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "site": {
- "alerts": [
- {
- "sourceid": "3",
- "wascid": "15",
- "cweid": "16",
- "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
- "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>",
- "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>",
- "count": "2",
- "pluginid": "10021",
- "alert": "X-Content-Type-Options Header Missing",
- "name": "X-Content-Type-Options Header Missing",
- "riskcode": "1",
- "confidence": "2",
- "riskdesc": "Low (Medium)",
- "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>",
- "instances": [
- {
- "param": "X-Content-Type-Options",
- "method": "GET",
- "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
- },
- {
- "param": "X-Content-Type-Options",
- "method": "GET",
- "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/"
- }
- ]
- }
- ],
- "@ssl": "false",
- "@port": "80",
- "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io",
- "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
- },
- "@generated": "Fri, 13 Apr 2018 09:22:01",
- "@version": "2.7.0"
-}
diff --git a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json
deleted file mode 100644
index 8555be6618c..00000000000
--- a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json
+++ /dev/null
@@ -1,181 +0,0 @@
-{
- "version": "1.3",
- "vulnerabilities": [
- {
- "category": "dependency_scanning",
- "name": "io.netty/netty - CVE-2014-3488",
- "message": "DoS by CPU exhaustion when using malicious SSL packets",
- "cve": "app/pom.xml:io.netty/netty@3.9.1.Final:CVE-2014-3488",
- "severity": "Unknown",
- "solution": "Upgrade to the latest version",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "app/pom.xml",
- "dependency": {
- "package": {
- "name": "io.netty/netty"
- },
- "version": "3.9.1.Final"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-d1bf36d9-9f07-46cd-9cfc-8675338ada8f",
- "value": "d1bf36d9-9f07-46cd-9cfc-8675338ada8f",
- "url": "https://deps.sec.gitlab.com/packages/maven/io.netty/netty/versions/3.9.1.Final/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2014-3488",
- "value": "CVE-2014-3488",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3488"
- }
- ],
- "links": [
- {
- "url": "https://bugzilla.redhat.com/CVE-2014-3488"
- },
- {
- "url": "http://netty.io/news/2014/06/11/3.html"
- },
- {
- "url": "https://github.com/netty/netty/issues/2562"
- }
- ],
- "priority": "Unknown",
- "file": "app/pom.xml",
- "url": "https://bugzilla.redhat.com/CVE-2014-3488",
- "tool": "gemnasium"
- },
- {
- "category": "dependency_scanning",
- "name": "Django - CVE-2017-12794",
- "message": "Possible XSS in traceback section of technical 500 debug page",
- "cve": "app/requirements.txt:Django@1.11.3:CVE-2017-12794",
- "severity": "Unknown",
- "solution": "Upgrade to latest version or apply patch.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "app/requirements.txt",
- "dependency": {
- "package": {
- "name": "Django"
- },
- "version": "1.11.3"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-6162a015-8635-4a15-8d7c-dc9321db366f",
- "value": "6162a015-8635-4a15-8d7c-dc9321db366f",
- "url": "https://deps.sec.gitlab.com/packages/pypi/Django/versions/1.11.3/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2017-12794",
- "value": "CVE-2017-12794",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12794"
- }
- ],
- "links": [
- {
- "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/"
- }
- ],
- "priority": "Unknown",
- "file": "app/requirements.txt",
- "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/",
- "tool": "gemnasium"
- },
- {
- "category": "dependency_scanning",
- "name": "nokogiri - USN-3424-1",
- "message": "Vulnerabilities in libxml2",
- "cve": "rails/Gemfile.lock:nokogiri@1.8.0:USN-3424-1",
- "severity": "Unknown",
- "solution": "Upgrade to latest version.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "rails/Gemfile.lock",
- "dependency": {
- "package": {
- "name": "nokogiri"
- },
- "version": "1.8.0"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d",
- "value": "06565b64-486d-4326-b906-890d9915804d",
- "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
- },
- {
- "type": "usn",
- "name": "USN-3424-1",
- "value": "USN-3424-1",
- "url": "https://usn.ubuntu.com/3424-1/"
- }
- ],
- "links": [
- {
- "url": "https://github.com/sparklemotion/nokogiri/issues/1673"
- }
- ],
- "priority": "Unknown",
- "file": "rails/Gemfile.lock",
- "url": "https://github.com/sparklemotion/nokogiri/issues/1673",
- "tool": "gemnasium"
- },
- {
- "category": "dependency_scanning",
- "name": "ffi - CVE-2018-1000201",
- "message": "ruby-ffi DDL loading issue on Windows OS",
- "cve": "ffi:1.9.18:CVE-2018-1000201",
- "severity": "High",
- "solution": "upgrade to \u003e= 1.9.24",
- "scanner": {
- "id": "bundler_audit",
- "name": "bundler-audit"
- },
- "location": {
- "file": "sast-sample-rails/Gemfile.lock",
- "dependency": {
- "package": {
- "name": "ffi"
- },
- "version": "1.9.18"
- }
- },
- "identifiers": [
- {
- "type": "cve",
- "name": "CVE-2018-1000201",
- "value": "CVE-2018-1000201",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201"
- }
- ],
- "links": [
- {
- "url": "https://github.com/ffi/ffi/releases/tag/1.9.24"
- }
- ],
- "priority": "High",
- "file": "sast-sample-rails/Gemfile.lock",
- "url": "https://github.com/ffi/ffi/releases/tag/1.9.24",
- "tool": "bundler_audit"
- }
- ]
-}
diff --git a/spec/fixtures/security-reports/feature-branch/gl-license-management-report.json b/spec/fixtures/security-reports/feature-branch/gl-license-management-report.json
deleted file mode 100644
index 5fd81fd69bd..00000000000
--- a/spec/fixtures/security-reports/feature-branch/gl-license-management-report.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "licenses": [
- {
- "count": 1,
- "name": "WTFPL"
- },
- {
- "count": 1,
- "name": "MIT"
- }
- ],
- "dependencies": [
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "actioncable",
- "url": "http://rubyonrails.org",
- "description": "WebSocket framework for Rails.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "WTFPL",
- "url": "http://www.wtfpl.net/"
- },
- "dependency": {
- "name": "wtfpl_init",
- "url": "https://rubygems.org/gems/wtfpl_init",
- "description": "Download WTFPL license file and rename to LICENSE.md or something",
- "pathes": [
- "."
- ]
- }
- }
- ]
-}
diff --git a/spec/fixtures/security-reports/feature-branch/gl-sast-report.json b/spec/fixtures/security-reports/feature-branch/gl-sast-report.json
deleted file mode 100644
index 4bef3d22f70..00000000000
--- a/spec/fixtures/security-reports/feature-branch/gl-sast-report.json
+++ /dev/null
@@ -1,947 +0,0 @@
-{
- "version": "1.2",
- "vulnerabilities": [
- {
- "category": "sast",
- "message": "Probable insecure usage of temp file/directory.",
- "cve": "python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-tmp.py",
- "start_line": 1,
- "end_line": 1
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B108",
- "value": "B108",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
- }
- ],
- "priority": "Medium",
- "file": "python/hardcoded/hardcoded-tmp.py",
- "line": 1,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "name": "Predictable pseudorandom number generator",
- "message": "Predictable pseudorandom number generator",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 47,
- "end_line": 47,
- "class": "com.gitlab.security_products.tests.App",
- "method": "generateSecretToken2"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-PREDICTABLE_RANDOM",
- "value": "PREDICTABLE_RANDOM",
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 47,
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "name": "Predictable pseudorandom number generator",
- "message": "Predictable pseudorandom number generator",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 41,
- "end_line": 41,
- "class": "com.gitlab.security_products.tests.App",
- "method": "generateSecretToken1"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-PREDICTABLE_RANDOM",
- "value": "PREDICTABLE_RANDOM",
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 41,
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:cb203b465dffb0cb3a8e8bd8910b84b93b0a5995a938e4b903dbb0cd6ffa1254:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 11,
- "end_line": 11
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 11,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:a7173c43ae66bd07466632d819d450e0071e02dbf782763640d1092981f9631b:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 12,
- "end_line": 12
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 12,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:017017b77deb0b8369b6065947833eeea752a92ec8a700db590fece3e934cf0d:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 13,
- "end_line": 13
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 13,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:45fc8c53aea7b84f06bc4e590cc667678d6073c4c8a1d471177ca2146fb22db2:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 14,
- "end_line": 14
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 14,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Pickle library appears to be in use, possible security issue.",
- "cve": "python/imports/imports-aliases.py:5f200d47291e7bbd8352db23019b85453ca048dd98ea0c291260fa7d009963a4:B301",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 15,
- "end_line": 15
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B301",
- "value": "B301"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 15,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "name": "ECB mode is insecure",
- "message": "ECB mode is insecure",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:ECB_MODE",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 29,
- "end_line": 29,
- "class": "com.gitlab.security_products.tests.App",
- "method": "insecureCypher"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-ECB_MODE",
- "value": "ECB_MODE",
- "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 29,
- "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "name": "Cipher with no integrity",
- "message": "Cipher with no integrity",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:CIPHER_INTEGRITY",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 29,
- "end_line": 29,
- "class": "com.gitlab.security_products.tests.App",
- "method": "insecureCypher"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-CIPHER_INTEGRITY",
- "value": "CIPHER_INTEGRITY",
- "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 29,
- "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "message": "Probable insecure usage of temp file/directory.",
- "cve": "python/hardcoded/hardcoded-tmp.py:63dd4d626855555b816985d82c4614a790462a0a3ada89dc58eb97f9c50f3077:B108",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-tmp.py",
- "start_line": 14,
- "end_line": 14
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B108",
- "value": "B108",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
- }
- ],
- "priority": "Medium",
- "file": "python/hardcoded/hardcoded-tmp.py",
- "line": 14,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Probable insecure usage of temp file/directory.",
- "cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-tmp.py",
- "start_line": 10,
- "end_line": 10
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B108",
- "value": "B108",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
- }
- ],
- "priority": "Medium",
- "file": "python/hardcoded/hardcoded-tmp.py",
- "line": 10,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with Popen module.",
- "cve": "python/imports/imports-aliases.py:2c3e1fa1e54c3c6646e8bcfaee2518153c6799b77587ff8d9a7b0631f6d34785:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 1,
- "end_line": 1
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 1,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with pickle module.",
- "cve": "python/imports/imports.py:af58d07f6ad519ef5287fcae65bf1a6999448a1a3a8bc1ac2a11daa80d0b96bf:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports.py",
- "start_line": 2,
- "end_line": 2
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports.py",
- "line": 2,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with subprocess module.",
- "cve": "python/imports/imports.py:8de9bc98029d212db530785a5f6780cfa663548746ff228ab8fa96c5bb82f089:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports.py",
- "start_line": 4,
- "end_line": 4
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports.py",
- "line": 4,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'blerg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:97c30f1d76d2a88913e3ce9ae74087874d740f87de8af697a9c455f01119f633:B106",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 22,
- "end_line": 22
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B106",
- "value": "B106",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 22,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'root'",
- "cve": "python/hardcoded/hardcoded-passwords.py:7431c73a0bc16d94ece2a2e75ef38f302574d42c37ac0c3c38ad0b3bf8a59f10:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 5,
- "end_line": 5
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 5,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: ''",
- "cve": "python/hardcoded/hardcoded-passwords.py:d2d1857c27caedd49c57bfbcdc23afcc92bd66a22701fcdc632869aab4ca73ee:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 9,
- "end_line": 9
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 9,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'ajklawejrkl42348swfgkg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:fb3866215a61393a5c9c32a3b60e2058171a23219c353f722cbd3567acab21d2:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 13,
- "end_line": 13
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 13,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'blerg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:63c62a8b7e1e5224439bd26b28030585ac48741e28ca64561a6071080c560a5f:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 23,
- "end_line": 23
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 23,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'blerg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:4311b06d08df8fa58229b341c531da8e1a31ec4520597bdff920cd5c098d86f9:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 24,
- "end_line": 24
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 24,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with subprocess module.",
- "cve": "python/imports/imports-function.py:5858400c2f39047787702de44d03361ef8d954c9d14bd54ee1c2bef9e6a7df93:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-function.py",
- "start_line": 4,
- "end_line": 4
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-function.py",
- "line": 4,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with pickle module.",
- "cve": "python/imports/imports-function.py:dbda3cf4190279d30e0aad7dd137eca11272b0b225e8af4e8bf39682da67d956:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-function.py",
- "start_line": 2,
- "end_line": 2
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-function.py",
- "line": 2,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with Popen module.",
- "cve": "python/imports/imports-from.py:eb8a0db9cd1a8c1ab39a77e6025021b1261cc2a0b026b2f4a11fca4e0636d8dd:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-from.py",
- "start_line": 7,
- "end_line": 7
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-from.py",
- "line": 7,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell",
- "cve": "python/imports/imports-aliases.py:f99f9721e27537fbcb6699a4cf39c6740d6234d2c6f06cfc2d9ea977313c483d:B602",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 9,
- "end_line": 9
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B602",
- "value": "B602",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 9,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with subprocess module.",
- "cve": "python/imports/imports-from.py:332a12ab1146698f614a905ce6a6a5401497a12281aef200e80522711c69dcf4:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-from.py",
- "start_line": 6,
- "end_line": 6
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-from.py",
- "line": 6,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with Popen module.",
- "cve": "python/imports/imports-from.py:0a48de4a3d5348853a03666cb574697e3982998355e7a095a798bd02a5947276:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-from.py",
- "start_line": 1,
- "end_line": 2
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-from.py",
- "line": 1,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with pickle module.",
- "cve": "python/imports/imports-aliases.py:51b71661dff994bde3529639a727a678c8f5c4c96f00d300913f6d5be1bbdf26:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 7,
- "end_line": 8
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 7,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with loads module.",
- "cve": "python/imports/imports-aliases.py:6ff02aeb3149c01ab68484d794a94f58d5d3e3bb0d58557ef4153644ea68ea54:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 6,
- "end_line": 6
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 6,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)",
- "cve": "c/subdir/utils.c:b466873101951fe96e1332f6728eb7010acbbd5dfc3b65d7d53571d091a06d9e:CWE-119!/CWE-120",
- "confidence": "Low",
- "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "c/subdir/utils.c",
- "start_line": 4
- },
- "identifiers": [
- {
- "type": "cwe",
- "name": "CWE-119",
- "value": "119",
- "url": "https://cwe.mitre.org/data/definitions/119.html"
- },
- {
- "type": "cwe",
- "name": "CWE-120",
- "value": "120",
- "url": "https://cwe.mitre.org/data/definitions/120.html"
- }
- ],
- "file": "c/subdir/utils.c",
- "line": 4,
- "url": "https://cwe.mitre.org/data/definitions/119.html",
- "tool": "flawfinder"
- },
- {
- "category": "sast",
- "message": "Check when opening files - can an attacker redirect it (via symlinks), force the opening of special file type (e.g., device files), move things around to create a race condition, control its ancestors, or change its contents? (CWE-362)",
- "cve": "c/subdir/utils.c:bab681140fcc8fc3085b6bba74081b44ea145c1c98b5e70cf19ace2417d30770:CWE-362",
- "confidence": "Low",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "c/subdir/utils.c",
- "start_line": 8
- },
- "identifiers": [
- {
- "type": "cwe",
- "name": "CWE-362",
- "value": "362",
- "url": "https://cwe.mitre.org/data/definitions/362.html"
- }
- ],
- "file": "c/subdir/utils.c",
- "line": 8,
- "url": "https://cwe.mitre.org/data/definitions/362.html",
- "tool": "flawfinder"
- },
- {
- "category": "sast",
- "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)",
- "cve": "cplusplus/src/hello.cpp:c8c6dd0afdae6814194cf0930b719f757ab7b379cf8f261e7f4f9f2f323a818a:CWE-119!/CWE-120",
- "confidence": "Low",
- "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "cplusplus/src/hello.cpp",
- "start_line": 6
- },
- "identifiers": [
- {
- "type": "cwe",
- "name": "CWE-119",
- "value": "119",
- "url": "https://cwe.mitre.org/data/definitions/119.html"
- },
- {
- "type": "cwe",
- "name": "CWE-120",
- "value": "120",
- "url": "https://cwe.mitre.org/data/definitions/120.html"
- }
- ],
- "file": "cplusplus/src/hello.cpp",
- "line": 6,
- "url": "https://cwe.mitre.org/data/definitions/119.html",
- "tool": "flawfinder"
- },
- {
- "category": "sast",
- "message": "Does not check for buffer overflows when copying to destination [MS-banned] (CWE-120)",
- "cve": "cplusplus/src/hello.cpp:331c04062c4fe0c7c486f66f59e82ad146ab33cdd76ae757ca41f392d568cbd0:CWE-120",
- "confidence": "Low",
- "solution": "Consider using snprintf, strcpy_s, or strlcpy (warning: strncpy easily misused)",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "cplusplus/src/hello.cpp",
- "start_line": 7
- },
- "identifiers": [
- {
- "type": "cwe",
- "name": "CWE-120",
- "value": "120",
- "url": "https://cwe.mitre.org/data/definitions/120.html"
- }
- ],
- "file": "cplusplus/src/hello.cpp",
- "line": 7,
- "url": "https://cwe.mitre.org/data/definitions/120.html",
- "tool": "flawfinder"
- }
- ]
-}
diff --git a/spec/fixtures/security-reports/master.zip b/spec/fixtures/security-reports/master.zip
deleted file mode 100644
index 2261b5a1674..00000000000
--- a/spec/fixtures/security-reports/master.zip
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/security-reports/master/gl-container-scanning-report.json b/spec/fixtures/security-reports/master/gl-container-scanning-report.json
deleted file mode 100644
index 03dfc647162..00000000000
--- a/spec/fixtures/security-reports/master/gl-container-scanning-report.json
+++ /dev/null
@@ -1,105 +0,0 @@
-{
- "image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff",
- "unapproved": [
- "CVE-2017-18269",
- "CVE-2017-16997",
- "CVE-2018-1000001",
- "CVE-2016-10228",
- "CVE-2018-18520",
- "CVE-2010-4052",
- "CVE-2018-16869",
- "CVE-2018-18311"
- ],
- "vulnerabilities": [
- {
- "featurename": "glibc",
- "featureversion": "2.24-11+deb9u3",
- "vulnerability": "CVE-2017-18269",
- "namespace": "debian:9",
- "description": "SSE2-optimized memmove implementation problem.",
- "link": "https://security-tracker.debian.org/tracker/CVE-2017-18269",
- "severity": "Defcon1",
- "fixedby": "2.24-11+deb9u4"
- },
- {
- "featurename": "glibc",
- "featureversion": "2.24-11+deb9u3",
- "vulnerability": "CVE-2017-16997",
- "namespace": "debian:9",
- "description": "elf/dl-load.c in the GNU C Library (aka glibc or libc6) 2.19 through 2.26 mishandles RPATH and RUNPATH containing $ORIGIN for a privileged (setuid or AT_SECURE) program, which allows local users to gain privileges via a Trojan horse library in the current working directory, related to the fillin_rpath and decompose_rpath functions. This is associated with misinterpretion of an empty RPATH/RUNPATH token as the \"./\" directory. NOTE: this configuration of RPATH/RUNPATH for a privileged program is apparently very uncommon; most likely, no such program is shipped with any common Linux distribution.",
- "link": "https://security-tracker.debian.org/tracker/CVE-2017-16997",
- "severity": "Critical",
- "fixedby": ""
- },
- {
- "featurename": "glibc",
- "featureversion": "2.24-11+deb9u3",
- "vulnerability": "CVE-2018-1000001",
- "namespace": "debian:9",
- "description": "In glibc 2.26 and earlier there is confusion in the usage of getcwd() by realpath() which can be used to write before the destination buffer leading to a buffer underflow and potential code execution.",
- "link": "https://security-tracker.debian.org/tracker/CVE-2018-1000001",
- "severity": "High",
- "fixedby": ""
- },
- {
- "featurename": "glibc",
- "featureversion": "2.24-11+deb9u3",
- "vulnerability": "CVE-2016-10228",
- "namespace": "debian:9",
- "description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.25 and earlier, when invoked with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.",
- "link": "https://security-tracker.debian.org/tracker/CVE-2016-10228",
- "severity": "Medium",
- "fixedby": ""
- },
- {
- "featurename": "elfutils",
- "featureversion": "0.168-1",
- "vulnerability": "CVE-2018-18520",
- "namespace": "debian:9",
- "description": "An Invalid Memory Address Dereference exists in the function elf_end in libelf in elfutils through v0.174. Although eu-size is intended to support ar files inside ar files, handle_ar in size.c closes the outer ar file before handling all inner entries. The vulnerability allows attackers to cause a denial of service (application crash) with a crafted ELF file.",
- "link": "https://security-tracker.debian.org/tracker/CVE-2018-18520",
- "severity": "Low",
- "fixedby": ""
- },
- {
- "featurename": "glibc",
- "featureversion": "2.24-11+deb9u3",
- "vulnerability": "CVE-2010-4052",
- "namespace": "debian:9",
- "description": "Stack consumption vulnerability in the regcomp implementation in the GNU C Library (aka glibc or libc6) through 2.11.3, and 2.12.x through 2.12.2, allows context-dependent attackers to cause a denial of service (resource exhaustion) via a regular expression containing adjacent repetition operators, as demonstrated by a {10,}{10,}{10,}{10,} sequence in the proftpd.gnu.c exploit for ProFTPD.",
- "link": "https://security-tracker.debian.org/tracker/CVE-2010-4052",
- "severity": "Negligible",
- "fixedby": ""
- },
- {
- "featurename": "nettle",
- "featureversion": "3.3-1",
- "vulnerability": "CVE-2018-16869",
- "namespace": "debian:9",
- "description": "A Bleichenbacher type side-channel based padding oracle attack was found in the way nettle handles endian conversion of RSA decrypted PKCS#1 v1.5 data. An attacker who is able to run a process on the same physical core as the victim process, could use this flaw extract plaintext or in some cases downgrade any TLS connections to a vulnerable server.",
- "link": "https://security-tracker.debian.org/tracker/CVE-2018-16869",
- "severity": "Unknown",
- "fixedby": ""
- },
- {
- "featurename": "perl",
- "featureversion": "5.24.1-3+deb9u4",
- "vulnerability": "CVE-2018-18311",
- "namespace": "debian:9",
- "description": "Perl before 5.26.3 and 5.28.x before 5.28.1 has a buffer overflow via a crafted regular expression that triggers invalid write operations.",
- "link": "https://security-tracker.debian.org/tracker/CVE-2018-18311",
- "severity": "Unknown",
- "fixedby": "5.24.1-3+deb9u5"
- },
- {
- "featurename": "foo",
- "featureversion": "1.3",
- "vulnerability": "CVE-2018-666",
- "namespace": "debian:9",
- "description": "Foo has a vulnerability nobody cares about and whitelist.",
- "link": "https://security-tracker.debian.org/tracker/CVE-2018-666",
- "severity": "Unknown",
- "fixedby": "1.4"
- }
- ]
-}
diff --git a/spec/fixtures/security-reports/master/gl-dast-report.json b/spec/fixtures/security-reports/master/gl-dast-report.json
deleted file mode 100644
index df459d9419d..00000000000
--- a/spec/fixtures/security-reports/master/gl-dast-report.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "site": [
- {
- "alerts": [
- {
- "sourceid": "3",
- "wascid": "15",
- "cweid": "16",
- "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
- "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>",
- "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>",
- "count": "2",
- "pluginid": "10021",
- "alert": "X-Content-Type-Options Header Missing",
- "name": "X-Content-Type-Options Header Missing",
- "riskcode": "1",
- "confidence": "2",
- "riskdesc": "Low (Medium)",
- "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>",
- "instances": [
- {
- "param": "X-Content-Type-Options",
- "method": "GET",
- "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
- },
- {
- "param": "X-Content-Type-Options",
- "method": "GET",
- "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/"
- }
- ]
- }
- ],
- "@ssl": "false",
- "@port": "80",
- "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io",
- "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
- }
- ],
- "@generated": "Fri, 13 Apr 2018 09:22:01",
- "@version": "2.7.0"
-}
diff --git a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
deleted file mode 100644
index 8555be6618c..00000000000
--- a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
+++ /dev/null
@@ -1,181 +0,0 @@
-{
- "version": "1.3",
- "vulnerabilities": [
- {
- "category": "dependency_scanning",
- "name": "io.netty/netty - CVE-2014-3488",
- "message": "DoS by CPU exhaustion when using malicious SSL packets",
- "cve": "app/pom.xml:io.netty/netty@3.9.1.Final:CVE-2014-3488",
- "severity": "Unknown",
- "solution": "Upgrade to the latest version",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "app/pom.xml",
- "dependency": {
- "package": {
- "name": "io.netty/netty"
- },
- "version": "3.9.1.Final"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-d1bf36d9-9f07-46cd-9cfc-8675338ada8f",
- "value": "d1bf36d9-9f07-46cd-9cfc-8675338ada8f",
- "url": "https://deps.sec.gitlab.com/packages/maven/io.netty/netty/versions/3.9.1.Final/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2014-3488",
- "value": "CVE-2014-3488",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3488"
- }
- ],
- "links": [
- {
- "url": "https://bugzilla.redhat.com/CVE-2014-3488"
- },
- {
- "url": "http://netty.io/news/2014/06/11/3.html"
- },
- {
- "url": "https://github.com/netty/netty/issues/2562"
- }
- ],
- "priority": "Unknown",
- "file": "app/pom.xml",
- "url": "https://bugzilla.redhat.com/CVE-2014-3488",
- "tool": "gemnasium"
- },
- {
- "category": "dependency_scanning",
- "name": "Django - CVE-2017-12794",
- "message": "Possible XSS in traceback section of technical 500 debug page",
- "cve": "app/requirements.txt:Django@1.11.3:CVE-2017-12794",
- "severity": "Unknown",
- "solution": "Upgrade to latest version or apply patch.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "app/requirements.txt",
- "dependency": {
- "package": {
- "name": "Django"
- },
- "version": "1.11.3"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-6162a015-8635-4a15-8d7c-dc9321db366f",
- "value": "6162a015-8635-4a15-8d7c-dc9321db366f",
- "url": "https://deps.sec.gitlab.com/packages/pypi/Django/versions/1.11.3/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2017-12794",
- "value": "CVE-2017-12794",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12794"
- }
- ],
- "links": [
- {
- "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/"
- }
- ],
- "priority": "Unknown",
- "file": "app/requirements.txt",
- "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/",
- "tool": "gemnasium"
- },
- {
- "category": "dependency_scanning",
- "name": "nokogiri - USN-3424-1",
- "message": "Vulnerabilities in libxml2",
- "cve": "rails/Gemfile.lock:nokogiri@1.8.0:USN-3424-1",
- "severity": "Unknown",
- "solution": "Upgrade to latest version.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "rails/Gemfile.lock",
- "dependency": {
- "package": {
- "name": "nokogiri"
- },
- "version": "1.8.0"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d",
- "value": "06565b64-486d-4326-b906-890d9915804d",
- "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
- },
- {
- "type": "usn",
- "name": "USN-3424-1",
- "value": "USN-3424-1",
- "url": "https://usn.ubuntu.com/3424-1/"
- }
- ],
- "links": [
- {
- "url": "https://github.com/sparklemotion/nokogiri/issues/1673"
- }
- ],
- "priority": "Unknown",
- "file": "rails/Gemfile.lock",
- "url": "https://github.com/sparklemotion/nokogiri/issues/1673",
- "tool": "gemnasium"
- },
- {
- "category": "dependency_scanning",
- "name": "ffi - CVE-2018-1000201",
- "message": "ruby-ffi DDL loading issue on Windows OS",
- "cve": "ffi:1.9.18:CVE-2018-1000201",
- "severity": "High",
- "solution": "upgrade to \u003e= 1.9.24",
- "scanner": {
- "id": "bundler_audit",
- "name": "bundler-audit"
- },
- "location": {
- "file": "sast-sample-rails/Gemfile.lock",
- "dependency": {
- "package": {
- "name": "ffi"
- },
- "version": "1.9.18"
- }
- },
- "identifiers": [
- {
- "type": "cve",
- "name": "CVE-2018-1000201",
- "value": "CVE-2018-1000201",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201"
- }
- ],
- "links": [
- {
- "url": "https://github.com/ffi/ffi/releases/tag/1.9.24"
- }
- ],
- "priority": "High",
- "file": "sast-sample-rails/Gemfile.lock",
- "url": "https://github.com/ffi/ffi/releases/tag/1.9.24",
- "tool": "bundler_audit"
- }
- ]
-}
diff --git a/spec/fixtures/security-reports/master/gl-license-management-report.json b/spec/fixtures/security-reports/master/gl-license-management-report.json
deleted file mode 100644
index e0de6f58fdf..00000000000
--- a/spec/fixtures/security-reports/master/gl-license-management-report.json
+++ /dev/null
@@ -1,817 +0,0 @@
-{
- "licenses": [
- {
- "count": 52,
- "name": "MIT"
- },
- {
- "count": 3,
- "name": "New BSD"
- },
- {
- "count": 1,
- "name": "Apache 2.0"
- },
- {
- "count": 1,
- "name": "unknown"
- }
- ],
- "dependencies": [
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "actioncable",
- "url": "http://rubyonrails.org",
- "description": "WebSocket framework for Rails.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "actionmailer",
- "url": "http://rubyonrails.org",
- "description": "Email composition, delivery, and receiving framework (part of Rails).",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "actionpack",
- "url": "http://rubyonrails.org",
- "description": "Web-flow and rendering framework putting the VC in MVC (part of Rails).",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "actionview",
- "url": "http://rubyonrails.org",
- "description": "Rendering framework putting the V in MVC (part of Rails).",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "activejob",
- "url": "http://rubyonrails.org",
- "description": "Job framework with pluggable queues.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "activemodel",
- "url": "http://rubyonrails.org",
- "description": "A toolkit for building modeling frameworks (part of Rails).",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "activerecord",
- "url": "http://rubyonrails.org",
- "description": "Object-relational mapper framework (part of Rails).",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "activesupport",
- "url": "http://rubyonrails.org",
- "description": "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "arel",
- "url": "https://github.com/rails/arel",
- "description": "Arel Really Exasperates Logicians Arel is a SQL AST manager for Ruby",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "builder",
- "url": "http://onestepback.org",
- "description": "Builders for MarkUp.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "bundler",
- "url": "http://bundler.io",
- "description": "The best way to manage your application's dependencies",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "coffee-rails",
- "url": "https://github.com/rails/coffee-rails",
- "description": "CoffeeScript adapter for the Rails asset pipeline.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "coffee-script",
- "url": "http://github.com/josh/ruby-coffee-script",
- "description": "Ruby CoffeeScript Compiler",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "coffee-script-source",
- "url": "http://coffeescript.org",
- "description": "The CoffeeScript Compiler",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "concurrent-ruby",
- "url": "http://www.concurrent-ruby.com",
- "description": "Modern concurrency tools for Ruby. Inspired by Erlang, Clojure, Scala, Haskell, F#, C#, Java, and classic concurrency patterns.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "crass",
- "url": "https://github.com/rgrove/crass/",
- "description": "CSS parser based on the CSS Syntax Level 3 spec.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "erubis",
- "url": "http://www.kuwata-lab.com/erubis/",
- "description": "a fast and extensible eRuby implementation which supports multi-language",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "execjs",
- "url": "https://github.com/rails/execjs",
- "description": "Run JavaScript code from Ruby",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "New BSD",
- "url": "http://opensource.org/licenses/BSD-3-Clause"
- },
- "dependency": {
- "name": "ffi",
- "url": "http://wiki.github.com/ffi/ffi",
- "description": "Ruby FFI",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "globalid",
- "url": "http://www.rubyonrails.org",
- "description": "Refer to any model with a URI: gid://app/class/id",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "i18n",
- "url": "http://github.com/svenfuchs/i18n",
- "description": "New wave Internationalization support for Ruby",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "jbuilder",
- "url": "https://github.com/rails/jbuilder",
- "description": "Create JSON structures via a Builder-style DSL",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "loofah",
- "description": "",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "mail",
- "url": "https://github.com/mikel/mail",
- "description": "Mail provides a nice Ruby DSL for making, sending and reading emails.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "method_source",
- "url": "http://banisterfiend.wordpress.com",
- "description": "retrieve the sourcecode for a method",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "mini_mime",
- "url": "https://github.com/discourse/mini_mime",
- "description": "A lightweight mime type lookup toy",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "mini_portile2",
- "url": "http://github.com/flavorjones/mini_portile",
- "description": "Simplistic port-like solution for developers",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "minitest",
- "url": "https://github.com/seattlerb/minitest",
- "description": "minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "multi_json",
- "url": "http://github.com/intridea/multi_json",
- "description": "A common interface to multiple JSON libraries.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "nio4r",
- "url": "https://github.com/celluloid/nio4r",
- "description": "NIO provides a high performance selector API for monitoring IO objects",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "nokogiri",
- "url": "http://nokogiri.org",
- "description": "Nokogiri (鋸) is an HTML, XML, SAX, and Reader parser",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "New BSD",
- "url": "http://opensource.org/licenses/BSD-3-Clause"
- },
- "dependency": {
- "name": "puma",
- "url": "http://puma.io",
- "description": "Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "rack",
- "url": "https://rack.github.io/",
- "description": "a modular Ruby webserver interface",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "rack-test",
- "url": "http://github.com/brynary/rack-test",
- "description": "Simple testing API built on Rack",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "rails",
- "url": "http://rubyonrails.org",
- "description": "Full-stack web application framework.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "rails-dom-testing",
- "url": "https://github.com/rails/rails-dom-testing",
- "description": "Dom and Selector assertions for Rails applications",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "rails-html-sanitizer",
- "url": "https://github.com/rails/rails-html-sanitizer",
- "description": "This gem is responsible to sanitize HTML fragments in Rails applications.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "railties",
- "url": "http://rubyonrails.org",
- "description": "Tools for creating, working with, and running Rails applications.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "rake",
- "url": "https://github.com/ruby/rake",
- "description": "Rake is a Make-like program implemented in Ruby",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "rb-fsevent",
- "url": "http://rubygems.org/gems/rb-fsevent",
- "description": "Very simple & usable FSEvents API",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "rb-inotify",
- "url": "https://github.com/guard/rb-inotify",
- "description": "A Ruby wrapper for Linux inotify, using FFI",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "unknown"
- },
- "dependency": {
- "name": "ruby-bundler-rails",
- "description": "",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "sass",
- "url": "http://sass-lang.com/",
- "description": "A powerful but elegant CSS compiler that makes CSS fun again.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "sass-listen",
- "url": "https://github.com/sass/listen",
- "description": "Fork of guard/listen",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "sass-rails",
- "url": "https://github.com/rails/sass-rails",
- "description": "Sass adapter for the Rails asset pipeline.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "sprockets",
- "url": "https://github.com/rails/sprockets",
- "description": "Rack-based asset packaging system",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "sprockets-rails",
- "url": "https://github.com/rails/sprockets-rails",
- "description": "Sprockets Rails integration",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "New BSD",
- "url": "http://opensource.org/licenses/BSD-3-Clause"
- },
- "dependency": {
- "name": "sqlite3",
- "url": "https://github.com/sparklemotion/sqlite3-ruby",
- "description": "This module allows Ruby programs to interface with the SQLite3 database engine (http://www.sqlite.org)",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "thor",
- "url": "http://whatisthor.com/",
- "description": "Thor is a toolkit for building powerful command-line interfaces.",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "Apache 2.0",
- "url": "http://www.apache.org/licenses/LICENSE-2.0.txt"
- },
- "dependency": {
- "name": "thread_safe",
- "url": "https://github.com/ruby-concurrency/thread_safe",
- "description": "Thread-safe collections and utilities for Ruby",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "tilt",
- "url": "http://github.com/rtomayko/tilt/",
- "description": "Generic interface to multiple Ruby template engines",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "turbolinks",
- "url": "https://github.com/turbolinks/turbolinks",
- "description": "Turbolinks makes navigating your web application faster",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "turbolinks-source",
- "url": "https://github.com/turbolinks/turbolinks-source-gem",
- "description": "Turbolinks JavaScript assets",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "tzinfo",
- "url": "http://tzinfo.github.io",
- "description": "Daylight savings aware timezone library",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "uglifier",
- "url": "http://github.com/lautis/uglifier",
- "description": "Ruby wrapper for UglifyJS JavaScript compressor",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "websocket-driver",
- "url": "http://github.com/faye/websocket-driver-ruby",
- "description": "WebSocket protocol handler with pluggable I/O",
- "pathes": [
- "."
- ]
- }
- },
- {
- "license": {
- "name": "MIT",
- "url": "http://opensource.org/licenses/mit-license"
- },
- "dependency": {
- "name": "websocket-extensions",
- "url": "https://github.com/faye/websocket-extensions-ruby",
- "description": "Generic extension manager for WebSocket connections",
- "pathes": [
- "."
- ]
- }
- }
- ]
-}
diff --git a/spec/fixtures/security-reports/master/gl-sast-report.json b/spec/fixtures/security-reports/master/gl-sast-report.json
deleted file mode 100644
index 345e1e9f83a..00000000000
--- a/spec/fixtures/security-reports/master/gl-sast-report.json
+++ /dev/null
@@ -1,967 +0,0 @@
-{
- "version": "1.2",
- "vulnerabilities": [
- {
- "category": "sast",
- "message": "Probable insecure usage of temp file/directory.",
- "cve": "python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-tmp.py",
- "start_line": 1,
- "end_line": 1
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B108",
- "value": "B108",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
- }
- ],
- "priority": "Medium",
- "file": "python/hardcoded/hardcoded-tmp.py",
- "line": 1,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "name": "Predictable pseudorandom number generator",
- "message": "Predictable pseudorandom number generator",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 47,
- "end_line": 47,
- "class": "com.gitlab.security_products.tests.App",
- "method": "generateSecretToken2"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-PREDICTABLE_RANDOM",
- "value": "PREDICTABLE_RANDOM",
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 47,
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "name": "Predictable pseudorandom number generator",
- "message": "Predictable pseudorandom number generator",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 41,
- "end_line": 41,
- "class": "com.gitlab.security_products.tests.App",
- "method": "generateSecretToken1"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-PREDICTABLE_RANDOM",
- "value": "PREDICTABLE_RANDOM",
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 41,
- "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:cb203b465dffb0cb3a8e8bd8910b84b93b0a5995a938e4b903dbb0cd6ffa1254:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 11,
- "end_line": 11
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 11,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:a7173c43ae66bd07466632d819d450e0071e02dbf782763640d1092981f9631b:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 12,
- "end_line": 12
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 12,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:017017b77deb0b8369b6065947833eeea752a92ec8a700db590fece3e934cf0d:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 13,
- "end_line": 13
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 13,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Use of insecure MD2, MD4, or MD5 hash function.",
- "cve": "python/imports/imports-aliases.py:45fc8c53aea7b84f06bc4e590cc667678d6073c4c8a1d471177ca2146fb22db2:B303",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 14,
- "end_line": 14
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B303",
- "value": "B303"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 14,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Pickle library appears to be in use, possible security issue.",
- "cve": "python/imports/imports-aliases.py:5f200d47291e7bbd8352db23019b85453ca048dd98ea0c291260fa7d009963a4:B301",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 15,
- "end_line": 15
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B301",
- "value": "B301"
- }
- ],
- "priority": "Medium",
- "file": "python/imports/imports-aliases.py",
- "line": 15,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "name": "ECB mode is insecure",
- "message": "ECB mode is insecure",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:ECB_MODE",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 29,
- "end_line": 29,
- "class": "com.gitlab.security_products.tests.App",
- "method": "insecureCypher"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-ECB_MODE",
- "value": "ECB_MODE",
- "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 29,
- "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "name": "Cipher with no integrity",
- "message": "Cipher with no integrity",
- "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:CIPHER_INTEGRITY",
- "severity": "Medium",
- "confidence": "High",
- "scanner": {
- "id": "find_sec_bugs",
- "name": "Find Security Bugs"
- },
- "location": {
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "start_line": 29,
- "end_line": 29,
- "class": "com.gitlab.security_products.tests.App",
- "method": "insecureCypher"
- },
- "identifiers": [
- {
- "type": "find_sec_bugs_type",
- "name": "Find Security Bugs-CIPHER_INTEGRITY",
- "value": "CIPHER_INTEGRITY",
- "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY"
- }
- ],
- "priority": "Medium",
- "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy",
- "line": 29,
- "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY",
- "tool": "find_sec_bugs"
- },
- {
- "category": "sast",
- "message": "Probable insecure usage of temp file/directory.",
- "cve": "python/hardcoded/hardcoded-tmp.py:63dd4d626855555b816985d82c4614a790462a0a3ada89dc58eb97f9c50f3077:B108",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-tmp.py",
- "start_line": 14,
- "end_line": 14
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B108",
- "value": "B108",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
- }
- ],
- "priority": "Medium",
- "file": "python/hardcoded/hardcoded-tmp.py",
- "line": 14,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Probable insecure usage of temp file/directory.",
- "cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108",
- "severity": "Medium",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-tmp.py",
- "start_line": 10,
- "end_line": 10
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B108",
- "value": "B108",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html"
- }
- ],
- "priority": "Medium",
- "file": "python/hardcoded/hardcoded-tmp.py",
- "line": 10,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with Popen module.",
- "cve": "python/imports/imports-aliases.py:2c3e1fa1e54c3c6646e8bcfaee2518153c6799b77587ff8d9a7b0631f6d34785:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 1,
- "end_line": 1
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 1,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with pickle module.",
- "cve": "python/imports/imports.py:af58d07f6ad519ef5287fcae65bf1a6999448a1a3a8bc1ac2a11daa80d0b96bf:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports.py",
- "start_line": 2,
- "end_line": 2
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports.py",
- "line": 2,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with subprocess module.",
- "cve": "python/imports/imports.py:8de9bc98029d212db530785a5f6780cfa663548746ff228ab8fa96c5bb82f089:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports.py",
- "start_line": 4,
- "end_line": 4
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports.py",
- "line": 4,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'blerg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:97c30f1d76d2a88913e3ce9ae74087874d740f87de8af697a9c455f01119f633:B106",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 22,
- "end_line": 22
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B106",
- "value": "B106",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 22,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'root'",
- "cve": "python/hardcoded/hardcoded-passwords.py:7431c73a0bc16d94ece2a2e75ef38f302574d42c37ac0c3c38ad0b3bf8a59f10:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 5,
- "end_line": 5
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 5,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: ''",
- "cve": "python/hardcoded/hardcoded-passwords.py:d2d1857c27caedd49c57bfbcdc23afcc92bd66a22701fcdc632869aab4ca73ee:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 9,
- "end_line": 9
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 9,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'ajklawejrkl42348swfgkg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:fb3866215a61393a5c9c32a3b60e2058171a23219c353f722cbd3567acab21d2:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 13,
- "end_line": 13
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 13,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'blerg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:63c62a8b7e1e5224439bd26b28030585ac48741e28ca64561a6071080c560a5f:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 23,
- "end_line": 23
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 23,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Possible hardcoded password: 'blerg'",
- "cve": "python/hardcoded/hardcoded-passwords.py:4311b06d08df8fa58229b341c531da8e1a31ec4520597bdff920cd5c098d86f9:B105",
- "severity": "Low",
- "confidence": "Medium",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/hardcoded/hardcoded-passwords.py",
- "start_line": 24,
- "end_line": 24
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B105",
- "value": "B105",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html"
- }
- ],
- "priority": "Low",
- "file": "python/hardcoded/hardcoded-passwords.py",
- "line": 24,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with subprocess module.",
- "cve": "python/imports/imports-function.py:5858400c2f39047787702de44d03361ef8d954c9d14bd54ee1c2bef9e6a7df93:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-function.py",
- "start_line": 4,
- "end_line": 4
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-function.py",
- "line": 4,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with pickle module.",
- "cve": "python/imports/imports-function.py:dbda3cf4190279d30e0aad7dd137eca11272b0b225e8af4e8bf39682da67d956:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-function.py",
- "start_line": 2,
- "end_line": 2
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-function.py",
- "line": 2,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with Popen module.",
- "cve": "python/imports/imports-from.py:eb8a0db9cd1a8c1ab39a77e6025021b1261cc2a0b026b2f4a11fca4e0636d8dd:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-from.py",
- "start_line": 7,
- "end_line": 7
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-from.py",
- "line": 7,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell",
- "cve": "python/imports/imports-aliases.py:f99f9721e27537fbcb6699a4cf39c6740d6234d2c6f06cfc2d9ea977313c483d:B602",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 9,
- "end_line": 9
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B602",
- "value": "B602",
- "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 9,
- "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html",
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with subprocess module.",
- "cve": "python/imports/imports-from.py:332a12ab1146698f614a905ce6a6a5401497a12281aef200e80522711c69dcf4:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-from.py",
- "start_line": 6,
- "end_line": 6
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-from.py",
- "line": 6,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with Popen module.",
- "cve": "python/imports/imports-from.py:0a48de4a3d5348853a03666cb574697e3982998355e7a095a798bd02a5947276:B404",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-from.py",
- "start_line": 1,
- "end_line": 2
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B404",
- "value": "B404"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-from.py",
- "line": 1,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with pickle module.",
- "cve": "python/imports/imports-aliases.py:51b71661dff994bde3529639a727a678c8f5c4c96f00d300913f6d5be1bbdf26:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 7,
- "end_line": 8
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 7,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Consider possible security implications associated with loads module.",
- "cve": "python/imports/imports-aliases.py:6ff02aeb3149c01ab68484d794a94f58d5d3e3bb0d58557ef4153644ea68ea54:B403",
- "severity": "Low",
- "confidence": "High",
- "scanner": {
- "id": "bandit",
- "name": "Bandit"
- },
- "location": {
- "file": "python/imports/imports-aliases.py",
- "start_line": 6,
- "end_line": 6
- },
- "identifiers": [
- {
- "type": "bandit_test_id",
- "name": "Bandit Test ID B403",
- "value": "B403"
- }
- ],
- "priority": "Low",
- "file": "python/imports/imports-aliases.py",
- "line": 6,
- "tool": "bandit"
- },
- {
- "category": "sast",
- "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)",
- "cve": "c/subdir/utils.c:b466873101951fe96e1332f6728eb7010acbbd5dfc3b65d7d53571d091a06d9e:CWE-119!/CWE-120",
- "confidence": "Low",
- "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "c/subdir/utils.c",
- "start_line": 4
- },
- "identifiers": [
- {
- "type": "flawfinder_func_name",
- "name": "Flawfinder - char",
- "value": "char"
- },
- {
- "type": "cwe",
- "name": "CWE-119",
- "value": "119",
- "url": "https://cwe.mitre.org/data/definitions/119.html"
- },
- {
- "type": "cwe",
- "name": "CWE-120",
- "value": "120",
- "url": "https://cwe.mitre.org/data/definitions/120.html"
- }
- ],
- "file": "c/subdir/utils.c",
- "line": 4,
- "url": "https://cwe.mitre.org/data/definitions/119.html",
- "tool": "flawfinder"
- },
- {
- "category": "sast",
- "message": "Check when opening files - can an attacker redirect it (via symlinks), force the opening of special file type (e.g., device files), move things around to create a race condition, control its ancestors, or change its contents? (CWE-362)",
- "cve": "c/subdir/utils.c:bab681140fcc8fc3085b6bba74081b44ea145c1c98b5e70cf19ace2417d30770:CWE-362",
- "confidence": "Low",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "c/subdir/utils.c",
- "start_line": 8
- },
- "identifiers": [
- {
- "type": "flawfinder_func_name",
- "name": "Flawfinder - fopen",
- "value": "fopen"
- },
- {
- "type": "cwe",
- "name": "CWE-362",
- "value": "362",
- "url": "https://cwe.mitre.org/data/definitions/362.html"
- }
- ],
- "file": "c/subdir/utils.c",
- "line": 8,
- "url": "https://cwe.mitre.org/data/definitions/362.html",
- "tool": "flawfinder"
- },
- {
- "category": "sast",
- "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)",
- "cve": "cplusplus/src/hello.cpp:c8c6dd0afdae6814194cf0930b719f757ab7b379cf8f261e7f4f9f2f323a818a:CWE-119!/CWE-120",
- "confidence": "Low",
- "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "cplusplus/src/hello.cpp",
- "start_line": 6
- },
- "identifiers": [
- {
- "type": "flawfinder_func_name",
- "name": "Flawfinder - char",
- "value": "char"
- },
- {
- "type": "cwe",
- "name": "CWE-119",
- "value": "119",
- "url": "https://cwe.mitre.org/data/definitions/119.html"
- },
- {
- "type": "cwe",
- "name": "CWE-120",
- "value": "120",
- "url": "https://cwe.mitre.org/data/definitions/120.html"
- }
- ],
- "file": "cplusplus/src/hello.cpp",
- "line": 6,
- "url": "https://cwe.mitre.org/data/definitions/119.html",
- "tool": "flawfinder"
- },
- {
- "category": "sast",
- "message": "Does not check for buffer overflows when copying to destination [MS-banned] (CWE-120)",
- "cve": "cplusplus/src/hello.cpp:331c04062c4fe0c7c486f66f59e82ad146ab33cdd76ae757ca41f392d568cbd0:CWE-120",
- "confidence": "Low",
- "solution": "Consider using snprintf, strcpy_s, or strlcpy (warning: strncpy easily misused)",
- "scanner": {
- "id": "flawfinder",
- "name": "Flawfinder"
- },
- "location": {
- "file": "cplusplus/src/hello.cpp",
- "start_line": 7
- },
- "identifiers": [
- {
- "type": "flawfinder_func_name",
- "name": "Flawfinder - strcpy",
- "value": "strcpy"
- },
- {
- "type": "cwe",
- "name": "CWE-120",
- "value": "120",
- "url": "https://cwe.mitre.org/data/definitions/120.html"
- }
- ],
- "file": "cplusplus/src/hello.cpp",
- "line": 7,
- "url": "https://cwe.mitre.org/data/definitions/120.html",
- "tool": "flawfinder"
- }
- ]
-}
diff --git a/spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json
deleted file mode 100644
index c96e831b027..00000000000
--- a/spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json
+++ /dev/null
@@ -1,104 +0,0 @@
-{
- "version": "2.0",
- "vulnerabilities": [
- {
- "category": "dependency_scanning",
- "name": "Regular Expression Denial of Service",
- "message": "Regular Expression Denial of Service in debug",
- "description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.",
- "cve": "yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a",
- "severity": "Unknown",
- "solution": "Upgrade to latest versions.",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "yarn.lock",
- "dependency": {
- "package": {
- "name": "debug"
- },
- "version": "1.0.5"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a",
- "value": "37283ed4-0380-40d7-ada7-2d994afcc62a",
- "url": "https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories"
- }
- ],
- "links": [
- {
- "url": "https://nodesecurity.io/advisories/534"
- },
- {
- "url": "https://github.com/visionmedia/debug/issues/501"
- },
- {
- "url": "https://github.com/visionmedia/debug/pull/504"
- }
- ]
- },
- {
- "category": "dependency_scanning",
- "name": "Authentication bypass via incorrect DOM traversal and canonicalization",
- "message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
- "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
- "cve": "yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98",
- "severity": "Unknown",
- "solution": "Upgrade to fixed version.\r\n",
- "scanner": {
- "id": "gemnasium",
- "name": "Gemnasium"
- },
- "location": {
- "file": "yarn.lock",
- "dependency": {
- "package": {
- "name": "saml2-js"
- },
- "version": "1.5.0"
- }
- },
- "identifiers": [
- {
- "type": "gemnasium",
- "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98",
- "value": "9952e574-7b5b-46fa-a270-aeb694198a98",
- "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories"
- },
- {
- "type": "cve",
- "name": "CVE-2017-11429",
- "value": "CVE-2017-11429",
- "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429"
- }
- ],
- "links": [
- {
- "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279"
- },
- {
- "url": "https://github.com/Clever/saml2/issues/127"
- },
- {
- "url": "https://www.kb.cert.org/vuls/id/475445"
- }
- ]
- }
- ],
- "remediations": [
- {
- "fixes": [
- {
- "cve": "yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98"
- }
- ],
- "summary": "Upgrade saml2-js",
- "diff": "ZGlmZiAtLWdpdCBhL3lhcm4ubG9jayBiL3lhcm4ubG9jawppbmRleCAwZWNjOTJmLi43ZmE0NTU0IDEwMDY0NAotLS0gYS95YXJuLmxvY2sKKysrIGIveWFybi5sb2NrCkBAIC0yLDEwMyArMiwxMjQgQEAKICMgeWFybiBsb2NrZmlsZSB2MQogCiAKLWFzeW5jQH4wLjIuNzoKLSAgdmVyc2lvbiAiMC4yLjEwIgotICByZXNvbHZlZCAiaHR0cDovL3JlZ2lzdHJ5Lm5wbWpzLm9yZy9hc3luYy8tL2FzeW5jLTAuMi4xMC50Z3ojYjZiYmUwYjA2NzRiOWQ3MTk3MDhjYTM4ZGU4YzIzN2NiNTI2YzNkMSIKLQotYXN5bmNAfjEuNS4yOgotICB2ZXJzaW9uICIxLjUuMiIKLSAgcmVzb2x2ZWQgImh0dHA6Ly9yZWdpc3RyeS5ucG1qcy5vcmcvYXN5bmMvLS9hc3luYy0xLjUuMi50Z3ojZWM2YTYxYWU1NjQ4MGMwYzNjYjI0MWM5NTYxOGUyMDg5MmY5NjcyYSIKK2FzeW5jQF4yLjEuNSwgYXN5bmNAXjIuNS4wOgorICB2ZXJzaW9uICIyLjYuMSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vYXN5bmMvLS9hc3luYy0yLjYuMS50Z3ojYjI0NWEyM2NhNzE5MzAwNDRlYzUzZmE0NmFhMDBhM2U4N2M2YTYxMCIKKyAgaW50ZWdyaXR5IHNoYTUxMi1mTkVpTDIrQVp0NkFsQXcvMjlDcjBVRGU0c1JBSENwRUhoNTRXTXorQmI3UWZOY0Z3NGgzbG9vZnlKcExlUXM0WXg3eXVxdS8yZExnTTVoS09zNkhsUT09CisgIGRlcGVuZGVuY2llczoKKyAgICBsb2Rhc2ggIl40LjE3LjEwIgogCi1kZWJ1Z0BeMS4wLjQ6Ci0gIHZlcnNpb24gIjEuMC41IgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9kZWJ1Zy8tL2RlYnVnLTEuMC41LnRneiNmNzI0MTIxNzQzMGY5OWRlYzRjMmI0NzNlYWI5MjIyOGU4NzRjMmFjIgorZGVidWdAXjIuNi4wOgorICB2ZXJzaW9uICIyLjYuOSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vZGVidWcvLS9kZWJ1Zy0yLjYuOS50Z3ojNWQxMjg1MTVkZjEzNGZmMzI3ZTkwYTRjOTNmNGUwNzdhNTM2MzQxZiIKKyAgaW50ZWdyaXR5IHNoYTUxMi1iQzdFbHJkSmFKblBiQVArMUVvdFl2cVpzYjNlY2w1d2k2QmZpNkJKVFVjTm93cDZjdnNwZzBqWHpuUlRLRGptL0U3QWRnRkJWZUFQVk1OY0tHc0hNQT09CiAgIGRlcGVuZGVuY2llczoKICAgICBtcyAiMi4wLjAiCiAKLWVqc0B+MC44LjM6Ci0gIHZlcnNpb24gIjAuOC44IgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9lanMvLS9lanMtMC44LjgudGd6I2ZmZGM1NmRjYzM1ZDAyOTI2ZGQ1MGFkMTM0MzliYmM1NDA2MWQ1OTgiCitlanNAXjIuNS42OgorICB2ZXJzaW9uICIyLjYuMSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vZWpzLy0vZWpzLTIuNi4xLnRneiM0OThlYzBkNDk1NjU1YWJjNmYyM2NkNjE4NjhkOTI2NDY0MDcxYWEwIgorICBpbnRlZ3JpdHkgc2hhNTEyLTB4eTRBL3R3ZnJSQ25raGZrOEVyRGk1RHFkQXNBcWVHeGh0NHhrQ1Vyc3ZoaGJRTnM3RSs0alYwQ043K05LSVkwYUhFNzIrWHZxdEJJWHpEMzFaYlhRPT0KKworbG9kYXNoLW5vZGVAfjIuNC4xOgorICB2ZXJzaW9uICIyLjQuMSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vbG9kYXNoLW5vZGUvLS9sb2Rhc2gtbm9kZS0yLjQuMS50Z3ojZWE4MmY3YjEwMGM3MzNkMWE0MmFmNzY4MDFlNTA2MTA1ZTJhODBlYyIKKyAgaW50ZWdyaXR5IHNoYTEtNm9MM3NRREhNOUdrS3Zkb0FlVUdFRjRxZ093PQorCitsb2Rhc2hAXjQuMTcuMTA6CisgIHZlcnNpb24gIjQuMTcuMTEiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL2xvZGFzaC8tL2xvZGFzaC00LjE3LjExLnRneiNiMzllYTYyMjllZjYwN2VjZDg5ZTJjOGRmMTI1MzY4OTFjYWM5YjhkIgorICBpbnRlZ3JpdHkgc2hhNTEyLWNRS2g4aWdvNVFVaFo3bGczOERZV0F4TXZqU0FLRzBBOHdHU1ZpbVAwN1NJVUVLMlVPK2FyU1JLYlJaV3RlbE10TjVWMEhrd2g1cnlPdG8vU3NoWUlnPT0KIAogbXNAMi4wLjA6CiAgIHZlcnNpb24gIjIuMC4wIgogICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9tcy8tL21zLTIuMC4wLnRneiM1NjA4YWVhZGZjMDBiZTZjMjkwMWRmNWY5ODYxNzg4ZGUwZDU5N2M4IgorICBpbnRlZ3JpdHkgc2hhMS1WZ2l1cmZ3QXZtd3BBZDlmbUdGNGplRFZsOGc9CiAKLW5vZGUtZm9yZ2VAMC4yLjI0OgotICB2ZXJzaW9uICIwLjIuMjQiCi0gIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL25vZGUtZm9yZ2UvLS9ub2RlLWZvcmdlLTAuMi4yNC50Z3ojZmE2Zjg0NmY0MmZhOTNmNjNhMGEzMGM5ZmJmZjdiNGUxMzBlMDg1OCIKK25vZGUtZm9yZ2VAXjAuNy4wOgorICB2ZXJzaW9uICIwLjcuNiIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vbm9kZS1mb3JnZS8tL25vZGUtZm9yZ2UtMC43LjYudGd6I2ZkZjNiNDE4YWVlMWY5NGYwZWY2NDJjZDYzNDg2Yzc3Y2E5NzI0YWMiCisgIGludGVncml0eSBzaGE1MTItc29sMzBMVXB6MWpRRkJqT0t3Ymp4aWppRTNiNnBqZDc0WXdmRDBmSk9LUGpGK2ZPTktiMllnOHJZZ1M2K2JLNlZEbCsvd2ZyNElZcEM3akR6TFVJZnc9PQogCiBzYW1sMi1qc0BeMS41LjA6Ci0gIHZlcnNpb24gIjEuNS4wIgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9zYW1sMi1qcy8tL3NhbWwyLWpzLTEuNS4wLnRneiNjMGQyMjY4YTE3OWU3MzI5ZDI5ZWIyNWFhODJkZjU1MDM3NzRiMGQ5IgorICB2ZXJzaW9uICIxLjEyLjQiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3NhbWwyLWpzLy0vc2FtbDItanMtMS4xMi40LnRneiNjMjg4ZjIwYmRhNmQyYjkxMDczYjE2Yzk0ZWE3MmYyMjM0OWFjM2IzIgorICBpbnRlZ3JpdHkgc2hhMS13b2p5QzlwdEs1RUhPeGJKVHFjdklqU2F3N009CiAgIGRlcGVuZGVuY2llczoKLSAgICBhc3luYyAifjEuNS4yIgotICAgIGRlYnVnICJeMS4wLjQiCi0gICAgdW5kZXJzY29yZSAifjEuNi4wIgotICAgIHhtbC1jcnlwdG8gIl4wLjguMSIKLSAgICB4bWwtZW5jcnlwdGlvbiAifjAuNy40IgotICAgIHhtbDJqcyAifjAuNC4xIgotICAgIHhtbGJ1aWxkZXIgIn4yLjEuMCIKLSAgICB4bWxkb20gIn4wLjEuMTkiCisgICAgYXN5bmMgIl4yLjUuMCIKKyAgICBkZWJ1ZyAiXjIuNi4wIgorICAgIHVuZGVyc2NvcmUgIl4xLjguMCIKKyAgICB4bWwtY3J5cHRvICJeMC4xMC4wIgorICAgIHhtbC1lbmNyeXB0aW9uICJeMC4xMS4wIgorICAgIHhtbDJqcyAiXjAuNC4wIgorICAgIHhtbGJ1aWxkZXIgIn4yLjIuMCIKKyAgICB4bWxkb20gIl4wLjEuMCIKIAogc2F4QD49MC42LjA6CiAgIHZlcnNpb24gIjEuMi40IgogICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9zYXgvLS9zYXgtMS4yLjQudGd6IzI4MTYyMzRlMjM3OGJkZGM0ZTUzNTRmYWI1Y2FhODk1ZGY3MTAwZDkiCisgIGludGVncml0eSBzaGE1MTItTnFWRHY5VHBBTlVqRm0wTjh1TTVHeEwzNlVnS2k5L2F0WncreDdZRm5ROGNrd0ZHS3JsNHhYNHlXdHJleTNVSm01blAxa1VibllnTG9wcVdOU1JoV3c9PQogCi11bmRlcnNjb3JlQD49MS41Lng6Cit1bmRlcnNjb3JlQF4xLjguMDoKICAgdmVyc2lvbiAiMS45LjEiCiAgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3VuZGVyc2NvcmUvLS91bmRlcnNjb3JlLTEuOS4xLnRneiMwNmRjZTM0YTBlNjhhN2JhYmMyOWIzNjViOGU3NGI4OTI1MjAzOTYxIgorICBpbnRlZ3JpdHkgc2hhNTEyLTUvNGV0bkNrZDljOGd3Z293aTUvb20vbVlPNWFqQ2FPZ2R6ai9vVyswZVFWOVd4S0JEWnc1K3ljbUttZWFUWGpJblMvVzBCenBHTG8yeFIyYUJ3WmRnPT0KIAotdW5kZXJzY29yZUB+MS42LjA6Ci0gIHZlcnNpb24gIjEuNi4wIgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS91bmRlcnNjb3JlLy0vdW5kZXJzY29yZS0xLjYuMC50Z3ojOGIzOGIxMGNhY2RlZjYzMzM3YjhiMjRlNGZmODZkNDVhZWE1MjlhOCIKLQoteG1sLWNyeXB0b0BeMC44LjE6Ci0gIHZlcnNpb24gIjAuOC41IgotICByZXNvbHZlZCAiaHR0cDovL3JlZ2lzdHJ5Lm5wbWpzLm9yZy94bWwtY3J5cHRvLy0veG1sLWNyeXB0by0wLjguNS50Z3ojMmJiY2ZiM2ViMzNmM2E4MmEyMThiODIyYmY2NzJiNmIxYzIwZTUzOCIKK3htbC1jcnlwdG9AXjAuMTAuMDoKKyAgdmVyc2lvbiAiMC4xMC4xIgorICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS94bWwtY3J5cHRvLy0veG1sLWNyeXB0by0wLjEwLjEudGd6I2Y4MzJmNzRjY2Y1NmYyNGFmY2FlMTE2M2ExZmNhYjQ0ZDk2Nzc0YTgiCisgIGludGVncml0eSBzaGExLStETDNUTTlXOGtyOHJoRmpvZnlyUk5sbmRLZz0KICAgZGVwZW5kZW5jaWVzOgogICAgIHhtbGRvbSAiPTAuMS4xOSIKICAgICB4cGF0aC5qcyAiPj0wLjAuMyIKIAoteG1sLWVuY3J5cHRpb25AfjAuNy40OgotICB2ZXJzaW9uICIwLjcuNCIKLSAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veG1sLWVuY3J5cHRpb24vLS94bWwtZW5jcnlwdGlvbi0wLjcuNC50Z3ojNDI3OTFlYzY0ZDU1NmQyNDU1ZGNiOWRhMGE1NDEyMzY2NWFjNjVjNyIKK3htbC1lbmNyeXB0aW9uQF4wLjExLjA6CisgIHZlcnNpb24gIjAuMTEuMiIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veG1sLWVuY3J5cHRpb24vLS94bWwtZW5jcnlwdGlvbi0wLjExLjIudGd6I2MyMTdmNTUwOTU0N2UzNGI1MDBiODI5ZjJjMGJjYTg1Y2NhNzNhMjEiCisgIGludGVncml0eSBzaGE1MTItalZ2RVM3aTVvdmRPN04rTmpnbmNBMzI2eFlLamhxZUFubnZJZ1JuWTdST0xDZkZxRURMd1AwU3hwLzMwU0hHMEFYUVYxMDQ4VDV5aW5PRnl2d0dGemc9PQogICBkZXBlbmRlbmNpZXM6Ci0gICAgYXN5bmMgIn4wLjIuNyIKLSAgICBlanMgIn4wLjguMyIKLSAgICBub2RlLWZvcmdlICIwLjIuMjQiCisgICAgYXN5bmMgIl4yLjEuNSIKKyAgICBlanMgIl4yLjUuNiIKKyAgICBub2RlLWZvcmdlICJeMC43LjAiCiAgICAgeG1sZG9tICJ+MC4xLjE1IgotICAgIHhwYXRoICIwLjAuNSIKKyAgICB4cGF0aCAiMC4wLjI3IgogCi14bWwyanNAfjAuNC4xOgoreG1sMmpzQF4wLjQuMDoKICAgdmVyc2lvbiAiMC40LjE5IgogICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS94bWwyanMvLS94bWwyanMtMC40LjE5LnRneiM2ODZjMjBmMjEzMjA5ZTk0YWJmMGQxYmNmMWVmYWEyOTFjNzgyN2E3IgorICBpbnRlZ3JpdHkgc2hhNTEyLWVzWm5KWkpPaUpSOXdXS015dXZTRTF5NkRxNUxDdUphbnFoeHNsSDJieE02ZHVhaE5aK0hNcENMaEJRR1prYlg2eFJmOHgxWTJlSmxndDJxM3FvNDlRPT0KICAgZGVwZW5kZW5jaWVzOgogICAgIHNheCAiPj0wLjYuMCIKICAgICB4bWxidWlsZGVyICJ+OS4wLjEiCiAKLXhtbGJ1aWxkZXJAfjIuMS4wOgotICB2ZXJzaW9uICIyLjEuMCIKLSAgcmVzb2x2ZWQgImh0dHA6Ly9yZWdpc3RyeS5ucG1qcy5vcmcveG1sYnVpbGRlci8tL3htbGJ1aWxkZXItMi4xLjAudGd6IzZkZGFlMzE2ODNiNmRmMTIxMDBiMjlmYzhhMGQ0ZjQ2MzQ5YWJiZWQiCit4bWxidWlsZGVyQH4yLjIuMDoKKyAgdmVyc2lvbiAiMi4yLjEiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3htbGJ1aWxkZXIvLS94bWxidWlsZGVyLTIuMi4xLnRneiM5MzI2NDMwZjEzMGQ4NzQzNWQ0YzQwODY2NDNhYTI5MjZlMTA1YTMyIgorICBpbnRlZ3JpdHkgc2hhMS1reVpERHhNTmgwTmRURUNHWkRxaWttNFFXakk9CiAgIGRlcGVuZGVuY2llczoKLSAgICB1bmRlcnNjb3JlICI+PTEuNS54IgorICAgIGxvZGFzaC1ub2RlICJ+Mi40LjEiCiAKIHhtbGJ1aWxkZXJAfjkuMC4xOgogICB2ZXJzaW9uICI5LjAuNyIKLSAgcmVzb2x2ZWQgImh0dHA6Ly9yZWdpc3RyeS5ucG1qcy5vcmcveG1sYnVpbGRlci8tL3htbGJ1aWxkZXItOS4wLjcudGd6IzEzMmVlNjNkMmVjNTU2NWM1NTdlMjBmNGMyMmRmOWFjYTY4NmIxMGQiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3htbGJ1aWxkZXIvLS94bWxidWlsZGVyLTkuMC43LnRneiMxMzJlZTYzZDJlYzU1NjVjNTU3ZTIwZjRjMjJkZjlhY2E2ODZiMTBkIgorICBpbnRlZ3JpdHkgc2hhMS1FeTdtUFM3RlZseFZmaUQwd2kzNXJLYUdzUTA9CiAKIHhtbGRvbUA9MC4xLjE5OgogICB2ZXJzaW9uICIwLjEuMTkiCiAgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3htbGRvbS8tL3htbGRvbS0wLjEuMTkudGd6IzYzMWZjMDc3NzZlZmQ4NDExOGJmMjUxNzFiMzdlZDRkMDc1YTBhYmMiCisgIGludGVncml0eSBzaGExLVl4L0FkM2J2MkVFWXZ5VVhHemZ0VFFkYUNydz0KIAoteG1sZG9tQH4wLjEuMTUsIHhtbGRvbUB+MC4xLjE5OgoreG1sZG9tQF4wLjEuMCwgeG1sZG9tQH4wLjEuMTU6CiAgIHZlcnNpb24gIjAuMS4yNyIKICAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veG1sZG9tLy0veG1sZG9tLTAuMS4yNy50Z3ojZDUwMWY5N2IzYmRiNDAzYWY4ZWY5ZWNjMjA1NzMxODdhYWRhYzBlOSIKKyAgaW50ZWdyaXR5IHNoYTEtMVFINWV6dmJRRHI0NzU3TUlGY3hoNnJhd09rPQogCiB4cGF0aC5qc0A+PTAuMC4zOgogICB2ZXJzaW9uICIxLjEuMCIKICAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veHBhdGguanMvLS94cGF0aC5qcy0xLjEuMC50Z3ojMzgxNmE0NGVkNGJiMzUyMDkxMDgzZDAwMmEzODNkZDUxMDRhNWZmMSIKKyAgaW50ZWdyaXR5IHNoYTUxMi1qZytxa2ZTNEs4RTc5NjVzcWFVbDhtUm5nWGlLYjNXWkdmT05nRTE4cHIwM0ZVUWl1U1Y2RytFajR0UzU1QitySVFTRkVJdzNwaGRWQVE0cFBxTldmUT09CiAKLXhwYXRoQDAuMC41OgotICB2ZXJzaW9uICIwLjAuNSIKLSAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veHBhdGgvLS94cGF0aC0wLjAuNS50Z3ojNDU0MDM2ZjZlZjBmM2RmNWFmNWQ0YmE0YTExOWZiNzU2NzRiM2U2YyIKK3hwYXRoQDAuMC4yNzoKKyAgdmVyc2lvbiAiMC4wLjI3IgorICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS94cGF0aC8tL3hwYXRoLTAuMC4yNy50Z3ojZGQzNDIxZmJkY2M1NjQ2YWMzMmM0ODUzMWI0ZDdlOWQwYzJjZmE5MiIKKyAgaW50ZWdyaXR5IHNoYTUxMi1mZzAzV1J4dGtDVjZvaENsZVBOQUVDWXNtcEtLVHY1TDh5L1gzRG4xaFFyZWMzUE94MmpIWi8wUDJxUTZIdnNyVTFCbWVxWGNvZjNOR0d1ZUc2THh3UT09Cg=="
- }
- ]
-}
diff --git a/spec/fixtures/security-reports/remediations/remediation.patch b/spec/fixtures/security-reports/remediations/remediation.patch
deleted file mode 100644
index bbfb6874627..00000000000
--- a/spec/fixtures/security-reports/remediations/remediation.patch
+++ /dev/null
@@ -1,180 +0,0 @@
-diff --git a/yarn.lock b/yarn.lock
-index 0ecc92f..7fa4554 100644
---- a/yarn.lock
-+++ b/yarn.lock
-@@ -2,103 +2,124 @@
- # yarn lockfile v1
-
-
--async@~0.2.7:
-- version "0.2.10"
-- resolved "http://registry.npmjs.org/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
--
--async@~1.5.2:
-- version "1.5.2"
-- resolved "http://registry.npmjs.org/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-+async@^2.1.5, async@^2.5.0:
-+ version "2.6.1"
-+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
-+ integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==
-+ dependencies:
-+ lodash "^4.17.10"
-
--debug@^1.0.4:
-- version "1.0.5"
-- resolved "https://registry.yarnpkg.com/debug/-/debug-1.0.5.tgz#f7241217430f99dec4c2b473eab92228e874c2ac"
-+debug@^2.6.0:
-+ version "2.6.9"
-+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
-+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
- dependencies:
- ms "2.0.0"
-
--ejs@~0.8.3:
-- version "0.8.8"
-- resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.8.8.tgz#ffdc56dcc35d02926dd50ad13439bbc54061d598"
-+ejs@^2.5.6:
-+ version "2.6.1"
-+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0"
-+ integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==
-+
-+lodash-node@~2.4.1:
-+ version "2.4.1"
-+ resolved "https://registry.yarnpkg.com/lodash-node/-/lodash-node-2.4.1.tgz#ea82f7b100c733d1a42af76801e506105e2a80ec"
-+ integrity sha1-6oL3sQDHM9GkKvdoAeUGEF4qgOw=
-+
-+lodash@^4.17.10:
-+ version "4.17.11"
-+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
-+ integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
-
- ms@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
-+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
-
--node-forge@0.2.24:
-- version "0.2.24"
-- resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.2.24.tgz#fa6f846f42fa93f63a0a30c9fbff7b4e130e0858"
-+node-forge@^0.7.0:
-+ version "0.7.6"
-+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
-+ integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
-
- saml2-js@^1.5.0:
-- version "1.5.0"
-- resolved "https://registry.yarnpkg.com/saml2-js/-/saml2-js-1.5.0.tgz#c0d2268a179e7329d29eb25aa82df5503774b0d9"
-+ version "1.12.4"
-+ resolved "https://registry.yarnpkg.com/saml2-js/-/saml2-js-1.12.4.tgz#c288f20bda6d2b91073b16c94ea72f22349ac3b3"
-+ integrity sha1-wojyC9ptK5EHOxbJTqcvIjSaw7M=
- dependencies:
-- async "~1.5.2"
-- debug "^1.0.4"
-- underscore "~1.6.0"
-- xml-crypto "^0.8.1"
-- xml-encryption "~0.7.4"
-- xml2js "~0.4.1"
-- xmlbuilder "~2.1.0"
-- xmldom "~0.1.19"
-+ async "^2.5.0"
-+ debug "^2.6.0"
-+ underscore "^1.8.0"
-+ xml-crypto "^0.10.0"
-+ xml-encryption "^0.11.0"
-+ xml2js "^0.4.0"
-+ xmlbuilder "~2.2.0"
-+ xmldom "^0.1.0"
-
- sax@>=0.6.0:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
-+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
-
--underscore@>=1.5.x:
-+underscore@^1.8.0:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961"
-+ integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==
-
--underscore@~1.6.0:
-- version "1.6.0"
-- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
--
--xml-crypto@^0.8.1:
-- version "0.8.5"
-- resolved "http://registry.npmjs.org/xml-crypto/-/xml-crypto-0.8.5.tgz#2bbcfb3eb33f3a82a218b822bf672b6b1c20e538"
-+xml-crypto@^0.10.0:
-+ version "0.10.1"
-+ resolved "https://registry.yarnpkg.com/xml-crypto/-/xml-crypto-0.10.1.tgz#f832f74ccf56f24afcae1163a1fcab44d96774a8"
-+ integrity sha1-+DL3TM9W8kr8rhFjofyrRNlndKg=
- dependencies:
- xmldom "=0.1.19"
- xpath.js ">=0.0.3"
-
--xml-encryption@~0.7.4:
-- version "0.7.4"
-- resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-0.7.4.tgz#42791ec64d556d2455dcb9da0a54123665ac65c7"
-+xml-encryption@^0.11.0:
-+ version "0.11.2"
-+ resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-0.11.2.tgz#c217f5509547e34b500b829f2c0bca85cca73a21"
-+ integrity sha512-jVvES7i5ovdO7N+NjgncA326xYKjhqeAnnvIgRnY7ROLCfFqEDLwP0Sxp/30SHG0AXQV1048T5yinOFyvwGFzg==
- dependencies:
-- async "~0.2.7"
-- ejs "~0.8.3"
-- node-forge "0.2.24"
-+ async "^2.1.5"
-+ ejs "^2.5.6"
-+ node-forge "^0.7.0"
- xmldom "~0.1.15"
-- xpath "0.0.5"
-+ xpath "0.0.27"
-
--xml2js@~0.4.1:
-+xml2js@^0.4.0:
- version "0.4.19"
- resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
-+ integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
- dependencies:
- sax ">=0.6.0"
- xmlbuilder "~9.0.1"
-
--xmlbuilder@~2.1.0:
-- version "2.1.0"
-- resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.1.0.tgz#6ddae31683b6df12100b29fc8a0d4f46349abbed"
-+xmlbuilder@~2.2.0:
-+ version "2.2.1"
-+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-2.2.1.tgz#9326430f130d87435d4c4086643aa2926e105a32"
-+ integrity sha1-kyZDDxMNh0NdTECGZDqikm4QWjI=
- dependencies:
-- underscore ">=1.5.x"
-+ lodash-node "~2.4.1"
-
- xmlbuilder@~9.0.1:
- version "9.0.7"
-- resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
-+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
-+ integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
-
- xmldom@=0.1.19:
- version "0.1.19"
- resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc"
-+ integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw=
-
--xmldom@~0.1.15, xmldom@~0.1.19:
-+xmldom@^0.1.0, xmldom@~0.1.15:
- version "0.1.27"
- resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
-+ integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk=
-
- xpath.js@>=0.0.3:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1"
-+ integrity sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==
-
--xpath@0.0.5:
-- version "0.0.5"
-- resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.5.tgz#454036f6ef0f3df5af5d4ba4a119fb75674b3e6c"
-+xpath@0.0.27:
-+ version "0.0.27"
-+ resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92"
-+ integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==
diff --git a/spec/fixtures/security-reports/remediations/yarn.lock b/spec/fixtures/security-reports/remediations/yarn.lock
deleted file mode 100644
index 0ecc92fb711..00000000000
--- a/spec/fixtures/security-reports/remediations/yarn.lock
+++ /dev/null
@@ -1,104 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-async@~0.2.7:
- version "0.2.10"
- resolved "http://registry.npmjs.org/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
-
-async@~1.5.2:
- version "1.5.2"
- resolved "http://registry.npmjs.org/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-
-debug@^1.0.4:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/debug/-/debug-1.0.5.tgz#f7241217430f99dec4c2b473eab92228e874c2ac"
- dependencies:
- ms "2.0.0"
-
-ejs@~0.8.3:
- version "0.8.8"
- resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.8.8.tgz#ffdc56dcc35d02926dd50ad13439bbc54061d598"
-
-ms@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
-
-node-forge@0.2.24:
- version "0.2.24"
- resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.2.24.tgz#fa6f846f42fa93f63a0a30c9fbff7b4e130e0858"
-
-saml2-js@^1.5.0:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/saml2-js/-/saml2-js-1.5.0.tgz#c0d2268a179e7329d29eb25aa82df5503774b0d9"
- dependencies:
- async "~1.5.2"
- debug "^1.0.4"
- underscore "~1.6.0"
- xml-crypto "^0.8.1"
- xml-encryption "~0.7.4"
- xml2js "~0.4.1"
- xmlbuilder "~2.1.0"
- xmldom "~0.1.19"
-
-sax@>=0.6.0:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
-
-underscore@>=1.5.x:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961"
-
-underscore@~1.6.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
-
-xml-crypto@^0.8.1:
- version "0.8.5"
- resolved "http://registry.npmjs.org/xml-crypto/-/xml-crypto-0.8.5.tgz#2bbcfb3eb33f3a82a218b822bf672b6b1c20e538"
- dependencies:
- xmldom "=0.1.19"
- xpath.js ">=0.0.3"
-
-xml-encryption@~0.7.4:
- version "0.7.4"
- resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-0.7.4.tgz#42791ec64d556d2455dcb9da0a54123665ac65c7"
- dependencies:
- async "~0.2.7"
- ejs "~0.8.3"
- node-forge "0.2.24"
- xmldom "~0.1.15"
- xpath "0.0.5"
-
-xml2js@~0.4.1:
- version "0.4.19"
- resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
- dependencies:
- sax ">=0.6.0"
- xmlbuilder "~9.0.1"
-
-xmlbuilder@~2.1.0:
- version "2.1.0"
- resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.1.0.tgz#6ddae31683b6df12100b29fc8a0d4f46349abbed"
- dependencies:
- underscore ">=1.5.x"
-
-xmlbuilder@~9.0.1:
- version "9.0.7"
- resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
-
-xmldom@=0.1.19:
- version "0.1.19"
- resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc"
-
-xmldom@~0.1.15, xmldom@~0.1.19:
- version "0.1.27"
- resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
-
-xpath.js@>=0.0.3:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1"
-
-xpath@0.0.5:
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.5.tgz#454036f6ef0f3df5af5d4ba4a119fb75674b3e6c"
diff --git a/spec/frontend/autosave_spec.js b/spec/frontend/autosave_spec.js
index 4d9c8f96d62..33d402388c9 100644
--- a/spec/frontend/autosave_spec.js
+++ b/spec/frontend/autosave_spec.js
@@ -63,12 +63,15 @@ describe('Autosave', () => {
expect(field.trigger).toHaveBeenCalled();
});
- it('triggers native event', done => {
- autosave.field.get(0).addEventListener('change', () => {
- done();
- });
+ it('triggers native event', () => {
+ const fieldElement = autosave.field.get(0);
+ const eventHandler = jest.fn();
+ fieldElement.addEventListener('change', eventHandler);
Autosave.prototype.restore.call(autosave);
+
+ expect(eventHandler).toHaveBeenCalledTimes(1);
+ fieldElement.removeEventListener('change', eventHandler);
});
});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index a986bc49f28..b0bdd924921 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -94,6 +94,12 @@ describe('URL utility', () => {
it('adds and updates encoded params', () => {
expect(urlUtils.mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag');
});
+
+ it('treats "+" as "%20"', () => {
+ expect(urlUtils.mergeUrlParams({ ref: 'bogus' }, '?a=lorem+ipsum&ref=charlie')).toBe(
+ '?a=lorem%20ipsum&ref=bogus',
+ );
+ });
});
describe('removeParams', () => {
diff --git a/spec/frontend/mocks/mocks_helper_spec.js b/spec/frontend/mocks/mocks_helper_spec.js
index 34be110a7e3..b8bb02c2f43 100644
--- a/spec/frontend/mocks/mocks_helper_spec.js
+++ b/spec/frontend/mocks/mocks_helper_spec.js
@@ -46,7 +46,9 @@ describe('mocks_helper.js', () => {
readdir.sync.mockReturnValue([]);
setupManualMocks();
- readdir.mock.calls.forEach(call => {
+ const readdirSpy = readdir.sync;
+ expect(readdirSpy).toHaveBeenCalled();
+ readdirSpy.mock.calls.forEach(call => {
expect(call[1].deep).toBeLessThan(100);
});
});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
new file mode 100644
index 00000000000..452d4cd07cc
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -0,0 +1,85 @@
+import { shallowMount } from '@vue/test-utils';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { TEST_HOST } from 'helpers/test_constants';
+import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
+import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
+import userDataMock from '../../user_data_mock';
+
+const TOOLTIP_PLACEMENT = 'bottom';
+const { name: USER_NAME, username: USER_USERNAME } = userDataMock();
+const TEST_ISSUABLE_TYPE = 'merge_request';
+
+describe('AssigneeAvatarLink component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: userDataMock(),
+ showLess: true,
+ rootPath: TEST_HOST,
+ tooltipPlacement: TOOLTIP_PLACEMENT,
+ singleUser: false,
+ issuableType: TEST_ISSUABLE_TYPE,
+ ...props,
+ };
+
+ wrapper = shallowMount(AssigneeAvatarLink, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findTooltipText = () => wrapper.attributes('data-original-title');
+
+ it('has the root url present in the assigneeUrl method', () => {
+ createComponent();
+ const assigneeUrl = joinPaths(TEST_HOST, USER_USERNAME);
+
+ expect(wrapper.attributes().href).toEqual(assigneeUrl);
+ });
+
+ it('renders assignee avatar', () => {
+ createComponent();
+
+ expect(wrapper.find(AssigneeAvatar).props()).toEqual(
+ expect.objectContaining({
+ issuableType: TEST_ISSUABLE_TYPE,
+ user: userDataMock(),
+ }),
+ );
+ });
+
+ describe.each`
+ issuableType | tooltipHasName | canMerge | expected
+ ${'merge_request'} | ${true} | ${true} | ${USER_NAME}
+ ${'merge_request'} | ${true} | ${false} | ${`${USER_NAME} (cannot merge)`}
+ ${'merge_request'} | ${false} | ${true} | ${''}
+ ${'merge_request'} | ${false} | ${false} | ${'Cannot merge'}
+ ${'issue'} | ${true} | ${true} | ${USER_NAME}
+ ${'issue'} | ${true} | ${false} | ${USER_NAME}
+ ${'issue'} | ${false} | ${true} | ${''}
+ ${'issue'} | ${false} | ${false} | ${''}
+ `(
+ 'with $issuableType and tooltipHasName=$tooltipHasName and canMerge=$canMerge',
+ ({ issuableType, tooltipHasName, canMerge, expected }) => {
+ beforeEach(() => {
+ createComponent({
+ issuableType,
+ tooltipHasName,
+ user: {
+ ...userDataMock(),
+ can_merge: canMerge,
+ },
+ });
+ });
+
+ it('sets tooltip', () => {
+ expect(findTooltipText()).toBe(expected);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
new file mode 100644
index 00000000000..d60ae17733b
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js
@@ -0,0 +1,78 @@
+import { shallowMount } from '@vue/test-utils';
+import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+import userDataMock from '../../user_data_mock';
+
+const TEST_AVATAR = `${TEST_HOST}/avatar.png`;
+const TEST_DEFAULT_AVATAR_URL = `${TEST_HOST}/default/avatar/url.png`;
+
+describe('AssigneeAvatar', () => {
+ let origGon;
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: userDataMock(),
+ imgSize: 24,
+ issuableType: 'merge_request',
+ ...props,
+ };
+
+ wrapper = shallowMount(AssigneeAvatar, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ beforeEach(() => {
+ origGon = window.gon;
+ window.gon = { default_avatar_url: TEST_DEFAULT_AVATAR_URL };
+ });
+
+ afterEach(() => {
+ window.gon = origGon;
+ wrapper.destroy();
+ });
+
+ const findImg = () => wrapper.find('img');
+
+ it('does not show warning icon if assignee can merge', () => {
+ createComponent();
+
+ expect(wrapper.find('.merge-icon').exists()).toBe(false);
+ });
+
+ it('shows warning icon if assignee cannot merge', () => {
+ createComponent({
+ user: {
+ can_merge: false,
+ },
+ });
+
+ expect(wrapper.find('.merge-icon').exists()).toBe(true);
+ });
+
+ it('does not show warning icon for issuableType = "issue"', () => {
+ createComponent({
+ issuableType: 'issue',
+ });
+
+ expect(wrapper.find('.merge-icon').exists()).toBe(false);
+ });
+
+ it.each`
+ avatar | avatar_url | expected | desc
+ ${TEST_AVATAR} | ${null} | ${TEST_AVATAR} | ${'with avatar'}
+ ${null} | ${TEST_AVATAR} | ${TEST_AVATAR} | ${'with avatar_url'}
+ ${null} | ${null} | ${TEST_DEFAULT_AVATAR_URL} | ${'with no avatar'}
+ `('$desc', ({ avatar, avatar_url, expected }) => {
+ createComponent({
+ user: {
+ avatar,
+ avatar_url,
+ },
+ });
+
+ expect(findImg().attributes('src')).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
new file mode 100644
index 00000000000..ff0c8d181b5
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -0,0 +1,189 @@
+import { shallowMount } from '@vue/test-utils';
+import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
+import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
+import UsersMockHelper from 'helpers/user_mock_data_helper';
+
+const DEFAULT_MAX_COUNTER = 99;
+
+describe('CollapsedAssigneeList component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ users: [],
+ issuableType: 'merge_request',
+ ...props,
+ };
+
+ wrapper = shallowMount(CollapsedAssigneeList, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ const findNoUsersIcon = () => wrapper.find('i[aria-label=None]');
+ const findAvatarCounter = () => wrapper.find('.avatar-counter');
+ const findAssignees = () => wrapper.findAll(CollapsedAssignee);
+ const getTooltipTitle = () => wrapper.attributes('data-original-title');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('No assignees/users', () => {
+ beforeEach(() => {
+ createComponent({
+ users: [],
+ });
+ });
+
+ it('has no users', () => {
+ expect(findNoUsersIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('One assignee/user', () => {
+ let users;
+
+ beforeEach(() => {
+ users = UsersMockHelper.createNumberRandomUsers(1);
+ });
+
+ it('should not show no users icon', () => {
+ createComponent({ users });
+
+ expect(findNoUsersIcon().exists()).toBe(false);
+ });
+
+ it('has correct "cannot merge" tooltip when user cannot merge', () => {
+ users[0].can_merge = false;
+
+ createComponent({ users });
+
+ expect(getTooltipTitle()).toContain('cannot merge');
+ });
+
+ it('does not have "merge" word in tooltip if user can merge', () => {
+ users[0].can_merge = true;
+
+ createComponent({ users });
+
+ expect(getTooltipTitle()).not.toContain('merge');
+ });
+ });
+
+ describe('More than one assignees/users', () => {
+ let users;
+
+ beforeEach(() => {
+ users = UsersMockHelper.createNumberRandomUsers(2);
+
+ createComponent({ users });
+ });
+
+ it('has multiple-users class', () => {
+ expect(wrapper.classes('multiple-users')).toBe(true);
+ });
+
+ it('does not display an avatar count', () => {
+ expect(findAvatarCounter().exists()).toBe(false);
+ });
+
+ it('returns just two collapsed users', () => {
+ expect(findAssignees().length).toBe(2);
+ });
+ });
+
+ describe('More than two assignees/users', () => {
+ let users;
+ let userNames;
+
+ beforeEach(() => {
+ users = UsersMockHelper.createNumberRandomUsers(3);
+ userNames = users.map(x => x.name).join(', ');
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent({ users });
+ });
+
+ it('does display an avatar count', () => {
+ expect(findAvatarCounter().exists()).toBe(true);
+ expect(findAvatarCounter().text()).toEqual('+2');
+ });
+
+ it('returns one collapsed users', () => {
+ expect(findAssignees().length).toBe(1);
+ });
+ });
+
+ it('has corrent "no one can merge" tooltip when no one can merge', () => {
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = false;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toEqual(`${userNames} (no one can merge)`);
+ });
+
+ it('has correct "cannot merge" tooltip when one user can merge', () => {
+ users[0].can_merge = true;
+ users[1].can_merge = false;
+ users[2].can_merge = false;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toEqual(`${userNames} (1/3 can merge)`);
+ });
+
+ it('has correct "cannot merge" tooltip when more than one user can merge', () => {
+ users[0].can_merge = false;
+ users[1].can_merge = true;
+ users[2].can_merge = true;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toEqual(`${userNames} (2/3 can merge)`);
+ });
+
+ it('does not have "merge" in tooltip if everyone can merge', () => {
+ users[0].can_merge = true;
+ users[1].can_merge = true;
+ users[2].can_merge = true;
+
+ createComponent({
+ users,
+ });
+
+ expect(getTooltipTitle()).toEqual(userNames);
+ });
+
+ it('displays the correct avatar count', () => {
+ users = UsersMockHelper.createNumberRandomUsers(5);
+
+ createComponent({
+ users,
+ });
+
+ expect(findAvatarCounter().text()).toEqual(`+${users.length - 1}`);
+ });
+
+ it('displays the correct avatar count via a computed property if more than default max counter', () => {
+ users = UsersMockHelper.createNumberRandomUsers(100);
+
+ createComponent({
+ users,
+ });
+
+ expect(findAvatarCounter().text()).toEqual(`${DEFAULT_MAX_COUNTER}+`);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
new file mode 100644
index 00000000000..f9ca7bc1ecb
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js
@@ -0,0 +1,49 @@
+import { shallowMount } from '@vue/test-utils';
+import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue';
+import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
+import userDataMock from '../../user_data_mock';
+
+const TEST_USER = userDataMock();
+const TEST_ISSUABLE_TYPE = 'merge_request';
+
+describe('CollapsedAssignee assignee component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: userDataMock(),
+ issuableType: TEST_ISSUABLE_TYPE,
+ ...props,
+ };
+
+ wrapper = shallowMount(CollapsedAssignee, {
+ propsData,
+ sync: false,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has author name', () => {
+ createComponent();
+
+ expect(
+ wrapper
+ .find('.author')
+ .text()
+ .trim(),
+ ).toEqual(TEST_USER.name);
+ });
+
+ it('has assignee avatar', () => {
+ createComponent();
+
+ expect(wrapper.find(AssigneeAvatar).props()).toEqual({
+ imgSize: 24,
+ user: TEST_USER,
+ issuableType: TEST_ISSUABLE_TYPE,
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
new file mode 100644
index 00000000000..6398351834c
--- /dev/null
+++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js
@@ -0,0 +1,103 @@
+import { mount } from '@vue/test-utils';
+import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
+import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+import userDataMock from '../../user_data_mock';
+import UsersMockHelper from '../../../helpers/user_mock_data_helper';
+
+const DEFAULT_RENDER_COUNT = 5;
+
+describe('UncollapsedAssigneeList component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ users: [],
+ rootPath: TEST_HOST,
+ ...props,
+ };
+
+ wrapper = mount(UncollapsedAssigneeList, {
+ sync: false,
+ propsData,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findMoreButton = () => wrapper.find('.user-list-more button');
+
+ describe('One assignee/user', () => {
+ let user;
+
+ beforeEach(() => {
+ user = userDataMock();
+
+ createComponent({
+ users: [user],
+ });
+ });
+
+ it('only has one user', () => {
+ expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(1);
+ });
+
+ it('calls the AssigneeAvatarLink with the proper props', () => {
+ expect(wrapper.find(AssigneeAvatarLink).exists()).toBe(true);
+ expect(wrapper.find(AssigneeAvatarLink).props().tooltipPlacement).toEqual('left');
+ });
+
+ it('Shows one user with avatar, username and author name', () => {
+ expect(wrapper.text()).toContain(user.name);
+ expect(wrapper.text()).toContain(`@${user.username}`);
+ });
+ });
+
+ describe('n+ more label', () => {
+ describe('when users count is rendered users', () => {
+ beforeEach(() => {
+ createComponent({
+ users: UsersMockHelper.createNumberRandomUsers(DEFAULT_RENDER_COUNT),
+ });
+ });
+
+ it('does not show more label', () => {
+ expect(findMoreButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when more than rendered users', () => {
+ beforeEach(() => {
+ createComponent({
+ users: UsersMockHelper.createNumberRandomUsers(DEFAULT_RENDER_COUNT + 1),
+ });
+ });
+
+ it('shows "+1 more" label', () => {
+ expect(findMoreButton().text()).toBe('+ 1 more');
+ });
+
+ it('shows truncated users', () => {
+ expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT);
+ });
+
+ describe('when more button is clicked', () => {
+ beforeEach(() => {
+ findMoreButton().trigger('click');
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows "show less" label', () => {
+ expect(findMoreButton().text()).toBe('- show less');
+ });
+
+ it('shows all users', () => {
+ expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT + 1);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/user_data_mock.js b/spec/frontend/sidebar/user_data_mock.js
new file mode 100644
index 00000000000..8ad70bb3499
--- /dev/null
+++ b/spec/frontend/sidebar/user_data_mock.js
@@ -0,0 +1,9 @@
+export default () => ({
+ avatar_url: 'mock_path',
+ id: 1,
+ name: 'Root',
+ state: 'active',
+ username: 'root',
+ web_url: '',
+ can_merge: true,
+});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index df8a625319b..d52aeb1fe6b 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -93,3 +93,9 @@ Object.assign(global, {
clearTimeout(id);
},
});
+
+// make sure that each test actually tests something
+// see https://jestjs.io/docs/en/expect#expecthasassertions
+beforeEach(() => {
+ expect.hasAssertions();
+});
diff --git a/spec/frontend/wikis_spec.js b/spec/frontend/wikis_spec.js
new file mode 100644
index 00000000000..b2475488d97
--- /dev/null
+++ b/spec/frontend/wikis_spec.js
@@ -0,0 +1,74 @@
+import Wikis from '~/pages/projects/wikis/wikis';
+import { setHTMLFixture } from './helpers/fixtures';
+
+describe('Wikis', () => {
+ describe('setting the commit message when the title changes', () => {
+ const editFormHtmlFixture = args => `<form class="wiki-form ${
+ args.newPage ? 'js-new-wiki-page' : ''
+ }">
+ <input type="text" id="wiki_title" value="My title" />
+ <input type="text" id="wiki_message" />
+ </form>`;
+
+ let wikis;
+ let titleInput;
+ let messageInput;
+
+ describe('when the wiki page is being created', () => {
+ const formHtmlFixture = editFormHtmlFixture({ newPage: true });
+
+ beforeEach(() => {
+ setHTMLFixture(formHtmlFixture);
+
+ titleInput = document.getElementById('wiki_title');
+ messageInput = document.getElementById('wiki_message');
+ wikis = new Wikis();
+ });
+
+ it('binds an event listener to the title input', () => {
+ wikis.handleWikiTitleChange = jest.fn();
+
+ titleInput.dispatchEvent(new Event('keyup'));
+
+ expect(wikis.handleWikiTitleChange).toHaveBeenCalled();
+ });
+
+ it('sets the commit message when title changes', () => {
+ titleInput.value = 'My title';
+ messageInput.value = '';
+
+ titleInput.dispatchEvent(new Event('keyup'));
+
+ expect(messageInput.value).toEqual('Create My title');
+ });
+
+ it('replaces hyphens with spaces', () => {
+ titleInput.value = 'my-hyphenated-title';
+ titleInput.dispatchEvent(new Event('keyup'));
+
+ expect(messageInput.value).toEqual('Create my hyphenated title');
+ });
+ });
+
+ describe('when the wiki page is being updated', () => {
+ const formHtmlFixture = editFormHtmlFixture({ newPage: false });
+
+ beforeEach(() => {
+ setHTMLFixture(formHtmlFixture);
+
+ titleInput = document.getElementById('wiki_title');
+ messageInput = document.getElementById('wiki_message');
+ wikis = new Wikis();
+ });
+
+ it('sets the commit message when title changes, prefixing with "Update"', () => {
+ titleInput.value = 'My title';
+ messageInput.value = '';
+
+ titleInput.dispatchEvent(new Event('keyup'));
+
+ expect(messageInput.value).toEqual('Update My title');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index d4280d3ec2c..356e7a8f1fe 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -372,7 +372,7 @@ describe('diff_file_header', () => {
});
it('displays old and new path if the file was renamed', () => {
- props.diffFile.viewer.name = diffViewerModes.renamed;
+ props.diffFile.renamed_file = true;
vm = mountComponentWithStore(Component, { props, store });
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index 7e00fbf2745..e10426a9858 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
+import '~/behaviors/markdown/render_gfm';
import Description from '~/issue_show/components/description.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
@@ -91,6 +92,7 @@ describe('Description component', () => {
let TaskList;
beforeEach(() => {
+ vm.$destroy();
vm = mountComponent(
DescriptionComponent,
Object.assign({}, props, {
diff --git a/spec/javascripts/monitoring/charts/time_series_spec.js b/spec/javascripts/monitoring/charts/time_series_spec.js
new file mode 100644
index 00000000000..d145a64e8d0
--- /dev/null
+++ b/spec/javascripts/monitoring/charts/time_series_spec.js
@@ -0,0 +1,335 @@
+import { shallowMount } from '@vue/test-utils';
+import { createStore } from '~/monitoring/stores';
+import { GlLink } from '@gitlab/ui';
+import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
+import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
+import TimeSeries from '~/monitoring/components/charts/time_series.vue';
+import * as types from '~/monitoring/stores/mutation_types';
+import { TEST_HOST } from 'spec/test_constants';
+import MonitoringMock, { deploymentData, mockProjectPath } from '../mock_data';
+
+describe('Time series component', () => {
+ const mockSha = 'mockSha';
+ const mockWidgets = 'mockWidgets';
+ const mockSvgPathContent = 'mockSvgPathContent';
+ const projectPath = `${TEST_HOST}${mockProjectPath}`;
+ const commitUrl = `${projectPath}/commit/${mockSha}`;
+ let mockGraphData;
+ let makeTimeSeriesChart;
+ let spriteSpy;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
+ store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
+ store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true });
+ [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics;
+
+ makeTimeSeriesChart = (graphData, type) =>
+ shallowMount(TimeSeries, {
+ propsData: {
+ graphData: { ...graphData, type },
+ containerWidth: 0,
+ deploymentData: store.state.monitoringDashboard.deploymentData,
+ projectPath,
+ },
+ slots: {
+ default: mockWidgets,
+ },
+ sync: false,
+ store,
+ });
+
+ spriteSpy = spyOnDependency(TimeSeries, 'getSvgIconPathContent').and.callFake(
+ () => new Promise(resolve => resolve(mockSvgPathContent)),
+ );
+ });
+
+ describe('general functions', () => {
+ let timeSeriesChart;
+
+ beforeEach(() => {
+ timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
+ });
+
+ it('renders chart title', () => {
+ expect(timeSeriesChart.find('.js-graph-title').text()).toBe(mockGraphData.title);
+ });
+
+ it('contains graph widgets from slot', () => {
+ expect(timeSeriesChart.find('.js-graph-widgets').text()).toBe(mockWidgets);
+ });
+
+ describe('when exportMetricsToCsvEnabled is disabled', () => {
+ beforeEach(() => {
+ store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false });
+ });
+
+ it('does not render the Download CSV button', done => {
+ timeSeriesChart.vm.$nextTick(() => {
+ expect(timeSeriesChart.contains('glbutton-stub')).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('formatTooltipText', () => {
+ const mockDate = deploymentData[0].created_at;
+ const mockCommitUrl = deploymentData[0].commitUrl;
+ const generateSeriesData = type => ({
+ seriesData: [
+ {
+ seriesName: timeSeriesChart.vm.chartData[0].name,
+ componentSubType: type,
+ value: [mockDate, 5.55555],
+ seriesIndex: 0,
+ },
+ ],
+ value: mockDate,
+ });
+
+ describe('when series is of line type', () => {
+ beforeEach(done => {
+ timeSeriesChart.vm.formatTooltipText(generateSeriesData('line'));
+ timeSeriesChart.vm.$nextTick(done);
+ });
+
+ it('formats tooltip title', () => {
+ expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
+ });
+
+ it('formats tooltip content', () => {
+ const name = 'Core Usage';
+ const value = '5.556';
+ const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
+
+ expect(seriesLabel.vm.color).toBe('');
+ expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
+ expect(timeSeriesChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]);
+ expect(
+ shallowWrapperContainsSlotText(
+ timeSeriesChart.find(GlAreaChart),
+ 'tooltipContent',
+ value,
+ ),
+ ).toBe(true);
+ });
+ });
+
+ describe('when series is of scatter type', () => {
+ beforeEach(() => {
+ timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
+ });
+
+ it('formats tooltip title', () => {
+ expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
+ });
+
+ it('formats tooltip sha', () => {
+ expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9');
+ });
+
+ it('formats tooltip commit url', () => {
+ expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl);
+ });
+ });
+ });
+
+ describe('setSvg', () => {
+ const mockSvgName = 'mockSvgName';
+
+ beforeEach(done => {
+ timeSeriesChart.vm.setSvg(mockSvgName);
+ timeSeriesChart.vm.$nextTick(done);
+ });
+
+ it('gets svg path content', () => {
+ expect(spriteSpy).toHaveBeenCalledWith(mockSvgName);
+ });
+
+ it('sets svg path content', () => {
+ timeSeriesChart.vm.$nextTick(() => {
+ expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`);
+ });
+ });
+ });
+
+ describe('onResize', () => {
+ const mockWidth = 233;
+
+ beforeEach(() => {
+ spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({
+ width: mockWidth,
+ }));
+ timeSeriesChart.vm.onResize();
+ });
+
+ it('sets area chart width', () => {
+ expect(timeSeriesChart.vm.width).toBe(mockWidth);
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe('chartData', () => {
+ let chartData;
+ const seriesData = () => chartData[0];
+
+ beforeEach(() => {
+ ({ chartData } = timeSeriesChart.vm);
+ });
+
+ it('utilizes all data points', () => {
+ const { values } = mockGraphData.queries[0].result[0];
+
+ expect(chartData.length).toBe(1);
+ expect(seriesData().data.length).toBe(values.length);
+ });
+
+ it('creates valid data', () => {
+ const { data } = seriesData();
+
+ expect(
+ data.filter(
+ ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number',
+ ).length,
+ ).toBe(data.length);
+ });
+
+ it('formats line width correctly', () => {
+ expect(chartData[0].lineStyle.width).toBe(2);
+ });
+ });
+
+ describe('chartOptions', () => {
+ describe('yAxis formatter', () => {
+ let format;
+
+ beforeEach(() => {
+ format = timeSeriesChart.vm.chartOptions.yAxis.axisLabel.formatter;
+ });
+
+ it('rounds to 3 decimal places', () => {
+ expect(format(0.88888)).toBe('0.889');
+ });
+ });
+ });
+
+ describe('scatterSeries', () => {
+ it('utilizes deployment data', () => {
+ expect(timeSeriesChart.vm.scatterSeries.data).toEqual([
+ ['2017-05-31T21:23:37.881Z', 0],
+ ['2017-05-30T20:08:04.629Z', 0],
+ ['2017-05-30T17:42:38.409Z', 0],
+ ]);
+
+ expect(timeSeriesChart.vm.scatterSeries.symbolSize).toBe(14);
+ });
+ });
+
+ describe('yAxisLabel', () => {
+ it('constructs a label for the chart y-axis', () => {
+ expect(timeSeriesChart.vm.yAxisLabel).toBe('CPU');
+ });
+ });
+
+ describe('csvText', () => {
+ it('converts data from json to csv', () => {
+ const header = `timestamp,${mockGraphData.y_label}`;
+ const data = mockGraphData.queries[0].result[0].values;
+ const firstRow = `${data[0][0]},${data[0][1]}`;
+
+ expect(timeSeriesChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`);
+ });
+ });
+
+ describe('downloadLink', () => {
+ it('produces a link to download metrics as csv', () => {
+ const link = timeSeriesChart.vm.downloadLink;
+
+ expect(link).toContain('blob:');
+ });
+ });
+ });
+
+ afterEach(() => {
+ timeSeriesChart.destroy();
+ });
+ });
+
+ describe('wrapped components', () => {
+ const glChartComponents = [
+ {
+ chartType: 'area-chart',
+ component: GlAreaChart,
+ },
+ {
+ chartType: 'line-chart',
+ component: GlLineChart,
+ },
+ ];
+
+ glChartComponents.forEach(dynamicComponent => {
+ describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
+ let timeSeriesAreaChart;
+ let glChart;
+
+ beforeEach(done => {
+ timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
+ glChart = timeSeriesAreaChart.find(dynamicComponent.component);
+ timeSeriesAreaChart.vm.$nextTick(done);
+ });
+
+ it('is a Vue instance', () => {
+ expect(glChart.exists()).toBe(true);
+ expect(glChart.isVueInstance()).toBe(true);
+ });
+
+ it('receives data properties needed for proper chart render', () => {
+ const props = glChart.props();
+
+ expect(props.data).toBe(timeSeriesAreaChart.vm.chartData);
+ expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions);
+ expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText);
+ expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds);
+ });
+
+ it('recieves a tooltip title', done => {
+ const mockTitle = 'mockTitle';
+ timeSeriesAreaChart.vm.tooltip.title = mockTitle;
+
+ timeSeriesAreaChart.vm.$nextTick(() => {
+ expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', mockTitle)).toBe(true);
+ done();
+ });
+ });
+
+ describe('when tooltip is showing deployment data', () => {
+ beforeEach(done => {
+ timeSeriesAreaChart.vm.tooltip.isDeployment = true;
+ timeSeriesAreaChart.vm.$nextTick(done);
+ });
+
+ it('uses deployment title', () => {
+ expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', 'Deployed')).toBe(true);
+ });
+
+ it('renders clickable commit sha in tooltip content', done => {
+ timeSeriesAreaChart.vm.tooltip.sha = mockSha;
+ timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl;
+
+ timeSeriesAreaChart.vm.$nextTick(() => {
+ const commitLink = timeSeriesAreaChart.find(GlLink);
+
+ expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
+ expect(commitLink.attributes('href')).toEqual(commitUrl);
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/components/dashboard_spec.js
index 624d8b14c8f..f3ec7520c6f 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/components/dashboard_spec.js
@@ -13,7 +13,7 @@ import MonitoringMock, {
environmentData,
singleGroupResponse,
dashboardGitResponse,
-} from './mock_data';
+} from '../mock_data';
const localVue = createLocalVue();
const propsData = {
@@ -414,6 +414,26 @@ describe('Dashboard', () => {
expect(clipboardText()).toContain(`y_label=`);
});
+ it('undefined parameter is stripped', done => {
+ wrapper.setProps({ currentDashboard: undefined });
+
+ wrapper.vm.$nextTick(() => {
+ expect(clipboardText()).not.toContain(`dashboard=`);
+ expect(clipboardText()).toContain(`y_label=`);
+ done();
+ });
+ });
+
+ it('null parameter is stripped', done => {
+ wrapper.setProps({ currentDashboard: null });
+
+ wrapper.vm.$nextTick(() => {
+ expect(clipboardText()).not.toContain(`dashboard=`);
+ expect(clipboardText()).toContain(`y_label=`);
+ done();
+ });
+ });
+
it('creates a toast when clicked', () => {
spyOn(wrapper.vm.$toast, 'show').and.stub();
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 85e660d3925..17e7314e214 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -1,5 +1,7 @@
export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`;
+export const mockProjectPath = '/frontend-fixtures/environments-project';
+
export const metricsGroupsAPIResponse = {
success: true,
data: [
@@ -902,7 +904,7 @@ export const metricsDashboardResponse = {
},
{
title: 'Memory Usage (Pod average)',
- type: 'area-chart',
+ type: 'line-chart',
y_label: 'Memory Used per Pod',
weight: 2,
metrics: [
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index 71dcba114a9..d69f469c7c7 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -14,6 +14,13 @@ import {
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
+// Helper function to ensure that we're using the same schema across tests.
+const createDiscussionNeighborParams = (discussionId, diffOrder, step) => ({
+ discussionId,
+ diffOrder,
+ step,
+});
+
describe('Getters Notes Store', () => {
let state;
@@ -25,7 +32,6 @@ describe('Getters Notes Store', () => {
targetNoteHash: 'hash',
lastFetchedAt: 'timestamp',
isNotesFetched: false,
-
notesData: notesDataMock,
userData: userDataMock,
noteableData: noteableDataMock,
@@ -244,62 +250,104 @@ describe('Getters Notes Store', () => {
});
});
- describe('nextUnresolvedDiscussionId', () => {
- const localGetters = {
- unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'],
- };
+ describe('findUnresolvedDiscussionIdNeighbor', () => {
+ let localGetters;
+ beforeEach(() => {
+ localGetters = {
+ unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'],
+ };
+ });
- it('should return the ID of the discussion after the ID provided', () => {
- expect(getters.nextUnresolvedDiscussionId(state, localGetters)('123')).toBe('456');
- expect(getters.nextUnresolvedDiscussionId(state, localGetters)('456')).toBe('789');
- expect(getters.nextUnresolvedDiscussionId(state, localGetters)('789')).toBe('123');
+ [
+ { step: 1, id: '123', expected: '456' },
+ { step: 1, id: '456', expected: '789' },
+ { step: 1, id: '789', expected: '123' },
+ { step: -1, id: '123', expected: '789' },
+ { step: -1, id: '456', expected: '123' },
+ { step: -1, id: '789', expected: '456' },
+ ].forEach(({ step, id, expected }) => {
+ it(`with step ${step} and id ${id}, returns next value`, () => {
+ const params = createDiscussionNeighborParams(id, true, step);
+
+ expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe(
+ expected,
+ );
+ });
});
- });
- describe('previousUnresolvedDiscussionId', () => {
- describe('with unresolved discussions', () => {
- const localGetters = {
- unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'],
- };
+ describe('with 1 unresolved discussion', () => {
+ beforeEach(() => {
+ localGetters = {
+ unresolvedDiscussionsIdsOrdered: () => ['123'],
+ };
+ });
+
+ [{ step: 1, id: '123', expected: '123' }, { step: -1, id: '123', expected: '123' }].forEach(
+ ({ step, id, expected }) => {
+ it(`with step ${step} and match, returns only value`, () => {
+ const params = createDiscussionNeighborParams(id, true, step);
- it('with bogus returns falsey', () => {
- expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('456');
+ expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe(
+ expected,
+ );
+ });
+ },
+ );
+
+ it('with no match, returns only value', () => {
+ const params = createDiscussionNeighborParams('bogus', true, 1);
+
+ expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe('123');
});
+ });
- [
- { id: '123', expected: '789' },
- { id: '456', expected: '123' },
- { id: '789', expected: '456' },
- ].forEach(({ id, expected }) => {
- it(`with ${id}, returns previous value`, () => {
- expect(getters.previousUnresolvedDiscussionId(state, localGetters)(id)).toBe(expected);
+ describe('with 0 unresolved discussions', () => {
+ beforeEach(() => {
+ localGetters = {
+ unresolvedDiscussionsIdsOrdered: () => [],
+ };
+ });
+
+ [{ step: 1 }, { step: -1 }].forEach(({ step }) => {
+ it(`with step ${step}, returns undefined`, () => {
+ const params = createDiscussionNeighborParams('bogus', true, step);
+
+ expect(
+ getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params),
+ ).toBeUndefined();
});
});
});
+ });
- describe('with 1 unresolved discussion', () => {
- const localGetters = {
- unresolvedDiscussionsIdsOrdered: () => ['123'],
- };
+ describe('findUnresolvedDiscussionIdNeighbor aliases', () => {
+ let neighbor;
+ let findUnresolvedDiscussionIdNeighbor;
+ let localGetters;
- it('with bogus returns id', () => {
- expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('123');
- });
+ beforeEach(() => {
+ neighbor = {};
+ findUnresolvedDiscussionIdNeighbor = jasmine.createSpy().and.returnValue(neighbor);
+ localGetters = { findUnresolvedDiscussionIdNeighbor };
+ });
- it('with match, returns value', () => {
- expect(getters.previousUnresolvedDiscussionId(state, localGetters)('123')).toEqual('123');
+ describe('nextUnresolvedDiscussionId', () => {
+ it('should return result of find neighbor', () => {
+ const expectedParams = createDiscussionNeighborParams('123', true, 1);
+ const result = getters.nextUnresolvedDiscussionId(state, localGetters)('123', true);
+
+ expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams);
+ expect(result).toBe(neighbor);
});
});
- describe('with 0 unresolved discussions', () => {
- const localGetters = {
- unresolvedDiscussionsIdsOrdered: () => [],
- };
+ describe('previosuUnresolvedDiscussionId', () => {
+ it('should return result of find neighbor', () => {
+ const expectedParams = createDiscussionNeighborParams('123', true, -1);
+ const result = getters.previousUnresolvedDiscussionId(state, localGetters)('123', true);
- it('returns undefined', () => {
- expect(
- getters.previousUnresolvedDiscussionId(state, localGetters)('bogus'),
- ).toBeUndefined();
+ expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams);
+ expect(result).toBe(neighbor);
});
});
});
diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js
index e7675669f7a..5ea3f85a247 100644
--- a/spec/javascripts/registry/components/app_spec.js
+++ b/spec/javascripts/registry/components/app_spec.js
@@ -84,12 +84,7 @@ describe('Registry List', () => {
it('should render empty message', done => {
setTimeout(() => {
- expect(
- vm.$el
- .querySelector('p')
- .textContent.trim()
- .replace(/[\r\n]+/g, ' '),
- ).toEqual(
+ expect(vm.$el.querySelector('.js-no-container-images-text').textContent).toEqual(
'With the Container Registry, every project can have its own space to store its Docker images. More Information',
);
done();
@@ -124,7 +119,9 @@ describe('Registry List', () => {
it('should render invalid characters error message', done => {
setTimeout(() => {
- expect(vm.$el.querySelector('.container-message')).not.toBe(null);
+ expect(vm.$el.querySelector('p')).not.toContain(
+ 'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More information',
+ );
done();
});
});
diff --git a/spec/javascripts/sidebar/assignee_title_spec.js b/spec/javascripts/sidebar/assignee_title_spec.js
index 509edba2036..7fff7c075d9 100644
--- a/spec/javascripts/sidebar/assignee_title_spec.js
+++ b/spec/javascripts/sidebar/assignee_title_spec.js
@@ -4,8 +4,10 @@ import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
describe('AssigneeTitle component', () => {
let component;
let AssigneeTitleComponent;
+ let statsSpy;
beforeEach(() => {
+ statsSpy = spyOnDependency(AssigneeTitle, 'trackEvent');
AssigneeTitleComponent = Vue.extend(AssigneeTitle);
});
@@ -102,4 +104,16 @@ describe('AssigneeTitle component', () => {
expect(component.$el.querySelector('.edit-link')).not.toBeNull();
});
+
+ it('calls trackEvent when edit is clicked', () => {
+ component = new AssigneeTitleComponent({
+ propsData: {
+ numberOfAssignees: 0,
+ editable: true,
+ },
+ }).$mount();
+ component.$el.querySelector('.js-sidebar-dropdown-toggle').click();
+
+ expect(statsSpy).toHaveBeenCalled();
+ });
});
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
index 4ae2141d5f0..a1df5389a38 100644
--- a/spec/javascripts/sidebar/assignees_spec.js
+++ b/spec/javascripts/sidebar/assignees_spec.js
@@ -94,115 +94,9 @@ describe('Assignee component', () => {
expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name);
});
-
- it('Shows one user with avatar, username and author name', () => {
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users: [UsersMock.user],
- editable: true,
- },
- }).$mount();
-
- expect(component.$el.querySelector('.author-link')).not.toBeNull();
- // The image
- expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual(
- UsersMock.user.avatar,
- );
- // Author name
- expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual(
- UsersMock.user.name,
- );
- // Username
- expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual(
- `@${UsersMock.user.username}`,
- );
- });
-
- it('has the root url present in the assigneeUrl method', () => {
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users: [UsersMock.user],
- editable: true,
- },
- }).$mount();
-
- expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual(
- -1,
- );
- });
-
- it('has correct "cannot merge" tooltip when user cannot merge', () => {
- const user = Object.assign({}, UsersMock.user, { can_merge: false });
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users: [user],
- editable: true,
- issuableType: 'merge_request',
- },
- }).$mount();
-
- expect(component.mergeNotAllowedTooltipMessage).toEqual('Cannot merge');
- });
});
describe('Two or more assignees/users', () => {
- it('has correct "cannot merge" tooltip when one user can merge', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- users[0].can_merge = true;
- users[1].can_merge = false;
- users[2].can_merge = false;
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users,
- editable: true,
- issuableType: 'merge_request',
- },
- }).$mount();
-
- expect(component.mergeNotAllowedTooltipMessage).toEqual('1/3 can merge');
- });
-
- it('has correct "cannot merge" tooltip when no user can merge', () => {
- const users = UsersMockHelper.createNumberRandomUsers(2);
- users[0].can_merge = false;
- users[1].can_merge = false;
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users,
- editable: true,
- issuableType: 'merge_request',
- },
- }).$mount();
-
- expect(component.mergeNotAllowedTooltipMessage).toEqual('No one can merge');
- });
-
- it('has correct "cannot merge" tooltip when more than one user can merge', () => {
- const users = UsersMockHelper.createNumberRandomUsers(3);
- users[0].can_merge = false;
- users[1].can_merge = true;
- users[2].can_merge = true;
-
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000/',
- users,
- editable: true,
- issuableType: 'merge_request',
- },
- }).$mount();
-
- expect(component.mergeNotAllowedTooltipMessage).toEqual('2/3 can merge');
- });
-
it('has no "cannot merge" tooltip when every user can merge', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
users[0].can_merge = true;
@@ -217,7 +111,7 @@ describe('Assignee component', () => {
},
}).$mount();
- expect(component.mergeNotAllowedTooltipMessage).toEqual(null);
+ expect(component.collapsedTooltipTitle).not.toContain('cannot merge');
});
it('displays two assignee icons when collapsed', () => {
@@ -295,8 +189,12 @@ describe('Assignee component', () => {
expect(component.$el.querySelector('.user-list-more')).toBe(null);
});
- it('sets tooltip container to body', () => {
- const users = UsersMockHelper.createNumberRandomUsers(2);
+ it('shows sorted assignee where "can merge" users are sorted first', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = true;
+
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
@@ -305,98 +203,46 @@ describe('Assignee component', () => {
},
}).$mount();
- expect(component.$el.querySelector('.user-link').getAttribute('data-container')).toBe('body');
+ expect(component.sortedAssigness[0].can_merge).toBe(true);
});
- it('Shows the "show-less" assignees label', done => {
- const users = UsersMockHelper.createNumberRandomUsers(6);
+ it('passes the sorted assignees to the uncollapsed-assignee-list', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = true;
+
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
- editable: true,
+ editable: false,
},
}).$mount();
- expect(component.$el.querySelectorAll('.user-item').length).toEqual(
- component.defaultRenderCount,
- );
-
- expect(component.$el.querySelector('.user-list-more')).not.toBe(null);
- const usersLabelExpectation = users.length - component.defaultRenderCount;
+ const userItems = component.$el.querySelectorAll('.user-list .user-item a');
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe(
- `+${usersLabelExpectation} more`,
- );
- component.toggleShowLess();
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
- '- show less',
- );
- done();
- });
+ expect(userItems.length).toBe(3);
+ expect(userItems[0].dataset.originalTitle).toBe(users[2].name);
});
- it('Shows the "show-less" when "n+ more " label is clicked', done => {
- const users = UsersMockHelper.createNumberRandomUsers(6);
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users,
- editable: true,
- },
- }).$mount();
-
- component.$el.querySelector('.user-list-more .btn-link').click();
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
- '- show less',
- );
- done();
- });
- });
+ it('passes the sorted assignees to the collapsed-assignee-list', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+ users[2].can_merge = true;
- it('gets the count of avatar via a computed property ', () => {
- const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
users,
- editable: true,
+ editable: false,
},
}).$mount();
- expect(component.sidebarAvatarCounter).toEqual(`+${users.length - 1}`);
- });
+ const collapsedButton = component.$el.querySelector('.sidebar-collapsed-user button');
- describe('n+ more label', () => {
- beforeEach(() => {
- const users = UsersMockHelper.createNumberRandomUsers(6);
- component = new AssigneeComponent({
- propsData: {
- rootPath: 'http://localhost:3000',
- users,
- editable: true,
- },
- }).$mount();
- });
-
- it('shows "+1 more" label', () => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
- '+ 1 more',
- );
- });
-
- it('shows "show less" label', done => {
- component.toggleShowLess();
-
- Vue.nextTick(() => {
- expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe(
- '- show less',
- );
- done();
- });
- });
+ expect(collapsedButton.innerText.trim()).toBe(users[2].name);
});
});
});
diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
index 486a7241e33..ea9e5677bc5 100644
--- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
@@ -4,8 +4,10 @@ import confidentialIssueSidebar from '~/sidebar/components/confidential/confiden
describe('Confidential Issue Sidebar Block', () => {
let vm1;
let vm2;
+ let statsSpy;
beforeEach(() => {
+ statsSpy = spyOnDependency(confidentialIssueSidebar, 'trackEvent');
const Component = Vue.extend(confidentialIssueSidebar);
const service = {
update: () => Promise.resolve(true),
@@ -67,4 +69,10 @@ describe('Confidential Issue Sidebar Block', () => {
done();
});
});
+
+ it('calls trackEvent when "Edit" is clicked', () => {
+ vm1.$el.querySelector('.confidential-edit').click();
+
+ expect(statsSpy).toHaveBeenCalled();
+ });
});
diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
index ca882032bdf..2d930428230 100644
--- a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
@@ -4,8 +4,10 @@ import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
describe('LockIssueSidebar', () => {
let vm1;
let vm2;
+ let statsSpy;
beforeEach(() => {
+ statsSpy = spyOnDependency(lockIssueSidebar, 'trackEvent');
const Component = Vue.extend(lockIssueSidebar);
const mediator = {
@@ -59,6 +61,12 @@ describe('LockIssueSidebar', () => {
});
});
+ it('calls trackEvent when "Edit" is clicked', () => {
+ vm1.$el.querySelector('.lock-edit').click();
+
+ expect(statsSpy).toHaveBeenCalled();
+ });
+
it('displays the edit form when opened from collapsed state', done => {
expect(vm1.isLockDialogOpen).toBe(false);
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
index 32728e58b06..2efa13f3fe8 100644
--- a/spec/javascripts/sidebar/subscriptions_spec.js
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -6,8 +6,10 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Subscriptions', function() {
let vm;
let Subscriptions;
+ let statsSpy;
beforeEach(() => {
+ statsSpy = spyOnDependency(subscriptions, 'trackEvent');
Subscriptions = Vue.extend(subscriptions);
});
@@ -58,6 +60,13 @@ describe('Subscriptions', function() {
expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
});
+ it('calls trackEvent when toggled', () => {
+ vm = mountComponent(Subscriptions, { subscribed: true });
+ vm.toggleSubscription();
+
+ expect(statsSpy).toHaveBeenCalled();
+ });
+
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
vm = mountComponent(Subscriptions, { subscribed: true });
spyOn(vm, '$emit');
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
index 212519743aa..7216ad00cc1 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
@@ -83,6 +83,24 @@ describe('Merge request widget rebase component', () => {
expect(text).toContain('foo');
expect(text.replace(/\s\s+/g, ' ')).toContain('to allow this merge request to be merged.');
});
+
+ it('should render the correct target branch name', () => {
+ const targetBranch = 'fake-branch-to-test-with';
+ vm = mountComponent(Component, {
+ mr: {
+ rebaseInProgress: false,
+ canPushToSourceBranch: false,
+ targetBranch,
+ },
+ service: {},
+ });
+
+ const elem = vm.$el.querySelector('.rebase-state-find-class-convention span');
+
+ expect(elem.innerHTML).toContain(
+ `Fast-forward merge is not possible. Rebase the source branch onto <span class="label-branch">${targetBranch}</span> to allow this merge request to be merged.`,
+ );
+ });
});
describe('methods', () => {
diff --git a/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js b/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js
new file mode 100644
index 00000000000..f1ca5f61496
--- /dev/null
+++ b/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js
@@ -0,0 +1,38 @@
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+
+/**
+ * We're testing this directive's hooks as pure functions
+ * since behaviour of this directive is highly-dependent
+ * on underlying DOM methods.
+ */
+describe('AutofocusOnShow directive', () => {
+ describe('with input invisible on component render', () => {
+ let el;
+
+ beforeAll(() => {
+ setFixtures('<div id="container" style="display: none;"><input id="inputel"/></div>');
+ el = document.querySelector('#inputel');
+ });
+
+ it('should bind IntersectionObserver on input element', () => {
+ spyOn(el, 'focus');
+
+ autofocusonshow.inserted(el);
+
+ expect(el.visibilityObserver).toBeDefined();
+ expect(el.focus).not.toHaveBeenCalled();
+ });
+
+ it('should stop IntersectionObserver on input element on unbind hook', () => {
+ el.visibilityObserver = {
+ disconnect: () => {},
+ };
+ spyOn(el.visibilityObserver, 'disconnect');
+
+ autofocusonshow.unbind(el);
+
+ expect(el.visibilityObserver).toBeDefined();
+ expect(el.visibilityObserver.disconnect).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/lib/gitlab/action_rate_limiter_spec.rb b/spec/lib/gitlab/action_rate_limiter_spec.rb
index 8dbad32dfb4..8b510a475d2 100644
--- a/spec/lib/gitlab/action_rate_limiter_spec.rb
+++ b/spec/lib/gitlab/action_rate_limiter_spec.rb
@@ -74,9 +74,9 @@ describe Gitlab::ActionRateLimiter, :clean_gitlab_redis_cache do
{
message: 'Action_Rate_Limiter_Request',
env: type,
- ip: '127.0.0.1',
+ remote_ip: '127.0.0.1',
request_method: 'GET',
- fullpath: fullpath
+ path: fullpath
}
end
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index f712f47a558..7140c14facb 100644
--- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -13,7 +13,12 @@ describe Gitlab::Ci::Build::Policy::Variables do
build(:ci_build, pipeline: pipeline, project: project, ref: 'master')
end
- let(:seed) { double('build seed', to_resource: ci_build) }
+ let(:seed) do
+ double('build seed',
+ to_resource: ci_build,
+ scoped_variables_hash: ci_build.scoped_variables_hash
+ )
+ end
before do
pipeline.variables.build(key: 'CI_PROJECT_NAME', value: '')
@@ -83,7 +88,12 @@ describe Gitlab::Ci::Build::Policy::Variables do
build(:ci_bridge, pipeline: pipeline, project: project, ref: 'master')
end
- let(:seed) { double('bridge seed', to_resource: bridge) }
+ let(:seed) do
+ double('bridge seed',
+ to_resource: bridge,
+ scoped_variables_hash: ci_build.scoped_variables_hash
+ )
+ end
it 'is satisfied by a matching expression for a bridge job' do
policy = described_class.new(['$MY_VARIABLE'])
diff --git a/spec/lib/gitlab/ci/build/rules/rule_spec.rb b/spec/lib/gitlab/ci/build/rules/rule_spec.rb
new file mode 100644
index 00000000000..99852bd4228
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/rules/rule_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Rules::Rule do
+ let(:seed) do
+ double('build seed',
+ to_resource: ci_build,
+ scoped_variables_hash: ci_build.scoped_variables_hash
+ )
+ end
+
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:ci_build) { build(:ci_build, pipeline: pipeline) }
+ let(:rule) { described_class.new(rule_hash) }
+
+ describe '#matches?' do
+ subject { rule.matches?(pipeline, seed) }
+
+ context 'with one matching clause' do
+ let(:rule_hash) do
+ { if: '$VAR == null', when: 'always' }
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with two matching clauses' do
+ let(:rule_hash) do
+ { if: '$VAR == null', changes: '**/*', when: 'always' }
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with a matching and non-matching clause' do
+ let(:rule_hash) do
+ { if: '$VAR != null', changes: '$VAR == null', when: 'always' }
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with two non-matching clauses' do
+ let(:rule_hash) do
+ { if: '$VAR != null', changes: 'README', when: 'always' }
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb
new file mode 100644
index 00000000000..d7793ebc806
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/rules_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Rules do
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:ci_build) { build(:ci_build, pipeline: pipeline) }
+
+ let(:seed) do
+ double('build seed',
+ to_resource: ci_build,
+ scoped_variables_hash: ci_build.scoped_variables_hash
+ )
+ end
+
+ let(:rules) { described_class.new(rule_list) }
+
+ describe '.new' do
+ let(:rules_ivar) { rules.instance_variable_get :@rule_list }
+ let(:default_when) { rules.instance_variable_get :@default_when }
+
+ context 'with no rules' do
+ let(:rule_list) { [] }
+
+ it 'sets @rule_list to an empty array' do
+ expect(rules_ivar).to eq([])
+ end
+
+ it 'sets @default_when to "on_success"' do
+ expect(default_when).to eq('on_success')
+ end
+ end
+
+ context 'with one rule' do
+ let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] }
+
+ it 'sets @rule_list to an array of a single rule' do
+ expect(rules_ivar).to be_an(Array)
+ end
+
+ it 'sets @default_when to "on_success"' do
+ expect(default_when).to eq('on_success')
+ end
+ end
+
+ context 'with multiple rules' do
+ let(:rule_list) do
+ [
+ { if: '$VAR == null', when: 'always' },
+ { if: '$VAR == null', when: 'always' }
+ ]
+ end
+
+ it 'sets @rule_list to an array of a single rule' do
+ expect(rules_ivar).to be_an(Array)
+ end
+
+ it 'sets @default_when to "on_success"' do
+ expect(default_when).to eq('on_success')
+ end
+ end
+
+ context 'with a specified default when:' do
+ let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] }
+ let(:rules) { described_class.new(rule_list, 'manual') }
+
+ it 'sets @rule_list to an array of a single rule' do
+ expect(rules_ivar).to be_an(Array)
+ end
+
+ it 'sets @default_when to "manual"' do
+ expect(default_when).to eq('manual')
+ end
+ end
+ end
+
+ describe '#evaluate' do
+ subject { rules.evaluate(pipeline, seed) }
+
+ context 'with nil rules' do
+ let(:rule_list) { nil }
+
+ it { is_expected.to eq(described_class::Result.new('on_success')) }
+
+ context 'and when:manual set as the default' do
+ let(:rules) { described_class.new(rule_list, 'manual') }
+
+ it { is_expected.to eq(described_class::Result.new('manual')) }
+ end
+ end
+
+ context 'with no rules' do
+ let(:rule_list) { [] }
+
+ it { is_expected.to eq(described_class::Result.new('never')) }
+
+ context 'and when:manual set as the default' do
+ let(:rules) { described_class.new(rule_list, 'manual') }
+
+ it { is_expected.to eq(described_class::Result.new('never')) }
+ end
+ end
+
+ context 'with one rule without any clauses' do
+ let(:rule_list) { [{ when: 'manual' }] }
+
+ it { is_expected.to eq(described_class::Result.new('manual')) }
+ end
+
+ context 'with one matching rule' do
+ let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] }
+
+ it { is_expected.to eq(described_class::Result.new('always')) }
+ end
+
+ context 'with two matching rules' do
+ let(:rule_list) do
+ [
+ { if: '$VAR == null', when: 'delayed', start_in: '1 day' },
+ { if: '$VAR == null', when: 'always' }
+ ]
+ end
+
+ it 'returns the value of the first matched rule in the list' do
+ expect(subject).to eq(described_class::Result.new('delayed', '1 day'))
+ end
+ end
+
+ context 'with a non-matching and matching rule' do
+ let(:rule_list) do
+ [
+ { if: '$VAR =! null', when: 'delayed', start_in: '1 day' },
+ { if: '$VAR == null', when: 'always' }
+ ]
+ end
+
+ it { is_expected.to eq(described_class::Result.new('always')) }
+ end
+
+ context 'with a matching and non-matching rule' do
+ let(:rule_list) do
+ [
+ { if: '$VAR == null', when: 'delayed', start_in: '1 day' },
+ { if: '$VAR != null', when: 'always' }
+ ]
+ end
+
+ it { is_expected.to eq(described_class::Result.new('delayed', '1 day')) }
+ end
+
+ context 'with non-matching rules' do
+ let(:rule_list) do
+ [
+ { if: '$VAR != null', when: 'delayed', start_in: '1 day' },
+ { if: '$VAR != null', when: 'always' }
+ ]
+ end
+
+ it { is_expected.to eq(described_class::Result.new('never')) }
+
+ context 'and when:manual set as the default' do
+ let(:rules) { described_class.new(rule_list, 'manual') }
+
+ it 'does not return the default when:' do
+ expect(subject).to eq(described_class::Result.new('never'))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 415ade7a096..1853efde350 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let(:result) do
%i[before_script script stage type after_script cache
- image services only except variables artifacts
+ image services only except rules variables artifacts
environment coverage retry]
end
@@ -201,6 +201,21 @@ describe Gitlab::Ci::Config::Entry::Job do
expect(entry.errors).to include 'job parallel must be an integer'
end
end
+
+ context 'when it uses both "when:" and "rules:"' do
+ let(:config) do
+ {
+ script: 'echo',
+ when: 'on_failure',
+ rules: [{ if: '$VARIABLE', when: 'on_success' }]
+ }
+ end
+
+ it 'returns an error about when: being combined with rules' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'job config key may not be used with `rules`: when'
+ end
+ end
end
context 'when delayed job' do
@@ -240,6 +255,100 @@ describe Gitlab::Ci::Config::Entry::Job do
end
end
+ context 'when only: is used with rules:' do
+ let(:config) { { only: ['merge_requests'], rules: [{ if: '$THIS' }] } }
+
+ it 'returns error about mixing only: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+
+ context 'and only: is blank' do
+ let(:config) { { only: nil, rules: [{ if: '$THIS' }] } }
+
+ it 'returns error about mixing only: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+
+ context 'and rules: is blank' do
+ let(:config) { { only: ['merge_requests'], rules: nil } }
+
+ it 'returns error about mixing only: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+ end
+
+ context 'when except: is used with rules:' do
+ let(:config) { { except: { refs: %w[master] }, rules: [{ if: '$THIS' }] } }
+
+ it 'returns error about mixing except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+
+ context 'and except: is blank' do
+ let(:config) { { except: nil, rules: [{ if: '$THIS' }] } }
+
+ it 'returns error about mixing except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+
+ context 'and rules: is blank' do
+ let(:config) { { except: { refs: %w[master] }, rules: nil } }
+
+ it 'returns error about mixing except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+ end
+
+ context 'when only: and except: are both used with rules:' do
+ let(:config) do
+ {
+ only: %w[merge_requests],
+ except: { refs: %w[master] },
+ rules: [{ if: '$THIS' }]
+ }
+ end
+
+ it 'returns errors about mixing both only: and except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+
+ context 'when only: and except: as both blank' do
+ let(:config) do
+ { only: nil, except: nil, rules: [{ if: '$THIS' }] }
+ end
+
+ it 'returns errors about mixing both only: and except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+
+ context 'when rules: is blank' do
+ let(:config) do
+ { only: %w[merge_requests], except: { refs: %w[master] }, rules: nil }
+ end
+
+ it 'returns errors about mixing both only: and except: with rules:' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include /may not be used with `rules`/
+ expect(entry.errors).to include /may not be used with `rules`/
+ end
+ end
+ end
+
context 'when start_in specified without delayed specification' do
let(:config) { { start_in: '1 day' } }
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 266a27c1e47..a606eb303e7 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -51,8 +51,6 @@ describe Gitlab::Ci::Config::Entry::Policy do
let(:config) { ['/^(?!master).+/'] }
- subject { described_class.new([regexp]) }
-
context 'when allow_unsafe_ruby_regexp is disabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
@@ -113,8 +111,6 @@ describe Gitlab::Ci::Config::Entry::Policy do
let(:config) { { refs: ['/^(?!master).+/'] } }
- subject { described_class.new([regexp]) }
-
context 'when allow_unsafe_ruby_regexp is disabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
@@ -204,6 +200,14 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
context 'when changes policy is invalid' do
+ let(:config) { { changes: 'some/*' } }
+
+ it 'returns errors' do
+ expect(entry.errors).to include /changes should be an array of strings/
+ end
+ end
+
+ context 'when changes policy is invalid' do
let(:config) { { changes: [1, 2] } }
it 'returns errors' do
diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
new file mode 100644
index 00000000000..c25344ec1a4
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
@@ -0,0 +1,208 @@
+require 'fast_spec_helper'
+require 'chronic_duration'
+require 'support/helpers/stub_feature_flags'
+require_dependency 'active_model'
+
+describe Gitlab::Ci::Config::Entry::Rules::Rule do
+ let(:entry) { described_class.new(config) }
+
+ describe '.new' do
+ subject { entry }
+
+ context 'with a when: value but no clauses' do
+ let(:config) { { when: 'manual' } }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when specifying an if: clause' do
+ let(:config) { { if: '$THIS || $THAT', when: 'manual' } }
+
+ it { is_expected.to be_valid }
+
+ describe '#when' do
+ subject { entry.when }
+
+ it { is_expected.to eq('manual') }
+ end
+ end
+
+ context 'using a list of multiple expressions' do
+ let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid format' do
+ expect(subject.errors).to include(/invalid expression syntax/)
+ end
+ end
+
+ context 'when specifying an invalid if: clause expression' do
+ let(:config) { { if: ['$MY_VAR =='] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid statement' do
+ expect(subject.errors).to include(/invalid expression syntax/)
+ end
+ end
+
+ context 'when specifying an if: clause expression with an invalid token' do
+ let(:config) { { if: ['$MY_VAR == 123'] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid statement' do
+ expect(subject.errors).to include(/invalid expression syntax/)
+ end
+ end
+
+ context 'when using invalid regex in an if: clause' do
+ let(:config) { { if: ['$MY_VAR =~ /some ( thing/'] } }
+
+ it 'reports an error about invalid expression' do
+ expect(subject.errors).to include(/invalid expression syntax/)
+ end
+ end
+
+ context 'when using an if: clause with lookahead regex character "?"' do
+ let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } }
+
+ context 'when allow_unsafe_ruby_regexp is disabled' do
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid expression syntax' do
+ expect(subject.errors).to include(/invalid expression syntax/)
+ end
+ end
+ end
+
+ context 'when using a changes: clause' do
+ let(:config) { { changes: %w[app/ lib/ spec/ other/* paths/**/*.rb] } }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when using a string as an invalid changes: clause' do
+ let(:config) { { changes: 'a regular string' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'reports an error about invalid policy' do
+ expect(subject.errors).to include(/should be an array of strings/)
+ end
+ end
+
+ context 'when using a list as an invalid changes: clause' do
+ let(:config) { { changes: [1, 2] } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns errors' do
+ expect(subject.errors).to include(/changes should be an array of strings/)
+ end
+ end
+
+ context 'specifying a delayed job' do
+ let(:config) { { if: '$THIS || $THAT', when: 'delayed', start_in: '15 minutes' } }
+
+ it { is_expected.to be_valid }
+
+ it 'sets attributes for the job delay' do
+ expect(entry.when).to eq('delayed')
+ expect(entry.start_in).to eq('15 minutes')
+ end
+
+ context 'without a when: key' do
+ let(:config) { { if: '$THIS || $THAT', start_in: '15 minutes' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about the disallowed key' do
+ expect(entry.errors).to include(/disallowed keys: start_in/)
+ end
+ end
+
+ context 'without a start_in: key' do
+ let(:config) { { if: '$THIS || $THAT', when: 'delayed' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about tstart_in being blank' do
+ expect(entry.errors).to include(/start in can't be blank/)
+ end
+ end
+ end
+
+ context 'when specifying unknown policy' do
+ let(:config) { { invalid: :something } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns error about invalid key' do
+ expect(entry.errors).to include(/unknown keys: invalid/)
+ end
+ end
+
+ context 'when clause is empty' do
+ let(:config) { {} }
+
+ it { is_expected.not_to be_valid }
+
+ it 'is not a valid configuration' do
+ expect(entry.errors).to include(/can't be blank/)
+ end
+ end
+
+ context 'when policy strategy does not match' do
+ let(:config) { 'string strategy' }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns information about errors' do
+ expect(entry.errors)
+ .to include(/should be a hash/)
+ end
+ end
+ end
+
+ describe '#value' do
+ subject { entry.value }
+
+ context 'when specifying an if: clause' do
+ let(:config) { { if: '$THIS || $THAT', when: 'manual' } }
+
+ it 'stores the expression as "if"' do
+ expect(subject).to eq(if: '$THIS || $THAT', when: 'manual')
+ end
+ end
+
+ context 'when using a changes: clause' do
+ let(:config) { { changes: %w[app/ lib/ spec/ other/* paths/**/*.rb] } }
+
+ it { is_expected.to eq(config) }
+ end
+
+ context 'when default value has been provided' do
+ let(:config) { { changes: %w[app/**/*.rb] } }
+
+ before do
+ entry.default = { changes: %w[**/*] }
+ end
+
+ it 'does not set a default value' do
+ expect(entry.default).to eq(nil)
+ end
+
+ it 'does not add to provided configuration' do
+ expect(entry.value).to eq(config)
+ end
+ end
+ end
+
+ describe '.default' do
+ it 'does not have default value' do
+ expect(described_class.default).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/rules_spec.rb
new file mode 100644
index 00000000000..291e7373daf
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/rules_spec.rb
@@ -0,0 +1,135 @@
+require 'fast_spec_helper'
+require 'support/helpers/stub_feature_flags'
+require_dependency 'active_model'
+
+describe Gitlab::Ci::Config::Entry::Rules do
+ let(:entry) { described_class.new(config) }
+
+ describe '.new' do
+ subject { entry }
+
+ context 'with a list of rule rule' do
+ let(:config) do
+ [{ if: '$THIS == "that"', when: 'never' }]
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.to be_valid }
+
+ context 'after #compose!' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'with a list of two rules' do
+ let(:config) do
+ [
+ { if: '$THIS == "that"', when: 'always' },
+ { if: '$SKIP', when: 'never' }
+ ]
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.to be_valid }
+
+ context 'after #compose!' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'with a single rule object' do
+ let(:config) do
+ { if: '$SKIP', when: 'never' }
+ end
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'with an invalid boolean when:' do
+ let(:config) do
+ [{ if: '$THIS == "that"', when: false }]
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.to be_valid }
+
+ context 'after #compose!' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: false/)
+ end
+ end
+ end
+
+ context 'with an invalid string when:' do
+ let(:config) do
+ [{ if: '$THIS == "that"', when: 'explode' }]
+ end
+
+ it { is_expected.to be_a(described_class) }
+ it { is_expected.to be_valid }
+
+ context 'after #compose!' do
+ before do
+ subject.compose!
+ end
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error about invalid when:' do
+ expect(subject.errors).to include(/when unknown value: explode/)
+ end
+ end
+ end
+ end
+
+ describe '#value' do
+ subject { entry.value }
+
+ context 'with a list of rule rule' do
+ let(:config) do
+ [{ if: '$THIS == "that"', when: 'never' }]
+ end
+
+ it { is_expected.to eq(config) }
+ end
+
+ context 'with a list of two rules' do
+ let(:config) do
+ [
+ { if: '$THIS == "that"', when: 'always' },
+ { if: '$SKIP', when: 'never' }
+ ]
+ end
+
+ it { is_expected.to eq(config) }
+ end
+
+ context 'with a single rule object' do
+ let(:config) do
+ { if: '$SKIP', when: 'never' }
+ end
+
+ it { is_expected.to eq(config) }
+ end
+ end
+
+ describe '.default' do
+ it 'does not have default policy' do
+ expect(described_class.default).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
index 4e4f1bf6ad3..a527783ffac 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
@@ -69,6 +69,34 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
it { is_expected.to eq(false) }
end
+ context 'when right is nil' do
+ let(:left_value) { 'my-awesome-string' }
+ let(:right_value) { nil }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when left and right are nil' do
+ let(:left_value) { nil }
+ let(:right_value) { nil }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when left is an empty string' do
+ let(:left_value) { '' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when left and right are empty strings' do
+ let(:left_value) { '' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('') }
+
+ it { is_expected.to eq(true) }
+ end
+
context 'when left is a multiline string and matches right' do
let(:left_value) do
<<~TEXT
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
index 6b81008ffb1..fb4238ecaf3 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
@@ -69,6 +69,34 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
it { is_expected.to eq(true) }
end
+ context 'when right is nil' do
+ let(:left_value) { 'my-awesome-string' }
+ let(:right_value) { nil }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when left and right are nil' do
+ let(:left_value) { nil }
+ let(:right_value) { nil }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when left is an empty string' do
+ let(:left_value) { '' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when left and right are empty strings' do
+ let(:left_value) { '' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('') }
+
+ it { is_expected.to eq(false) }
+ end
+
context 'when left is a multiline string and matches right' do
let(:left_value) do
<<~TEXT
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 1a9350d68bd..89431b80be3 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -15,6 +15,60 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.to be_a(Hash) }
it { is_expected.to include(:name, :project, :ref) }
+
+ context 'with job:when' do
+ let(:attributes) { { name: 'rspec', ref: 'master', when: 'on_failure' } }
+
+ it { is_expected.to include(when: 'on_failure') }
+ end
+
+ context 'with job:when:delayed' do
+ let(:attributes) { { name: 'rspec', ref: 'master', when: 'delayed', start_in: '3 hours' } }
+
+ it { is_expected.to include(when: 'delayed', start_in: '3 hours') }
+ end
+
+ context 'with job:rules:[when:]' do
+ context 'is matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'always' }] } }
+
+ it { is_expected.to include(when: 'always') }
+ end
+
+ context 'is not matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'always' }] } }
+
+ it { is_expected.to include(when: 'never') }
+ end
+ end
+
+ context 'with job:rules:[when:delayed]' do
+ context 'is matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } }
+
+ it { is_expected.to include(when: 'delayed', start_in: '3 hours') }
+ end
+
+ context 'is not matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'delayed', start_in: '3 hours' }] } }
+
+ it { is_expected.to include(when: 'never') }
+ end
+ end
+
+ context 'with job:rules but no explicit when:' do
+ context 'is matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null' }] } }
+
+ it { is_expected.to include(when: 'on_success') }
+ end
+
+ context 'is not matched' do
+ let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null' }] } }
+
+ it { is_expected.to include(when: 'never') }
+ end
+ end
end
describe '#bridge?' do
@@ -366,9 +420,25 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.not_to be_included }
end
+
+ context 'when using both only and except policies' do
+ let(:attributes) do
+ {
+ name: 'rspec',
+ only: {
+ refs: ["branches@#{pipeline.project_full_path}"]
+ },
+ except: {
+ refs: ["branches@#{pipeline.project_full_path}"]
+ }
+ }
+ end
+
+ it { is_expected.not_to be_included }
+ end
end
- context 'when repository path does not matches' do
+ context 'when repository path does not match' do
context 'when using only' do
let(:attributes) do
{ name: 'rspec', only: { refs: %w[branches@fork] } }
@@ -397,6 +467,215 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
it { is_expected.not_to be_included }
end
end
+
+ context 'using rules:' do
+ using RSpec::Parameterized
+
+ let(:attributes) { { name: 'rspec', rules: rule_set } }
+
+ context 'with a matching if: rule' do
+ context 'with an explicit `when: never`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null', when: 'never' }]],
+ [[{ if: '$VARIABLE == null', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]],
+ [[{ if: '$VARIABLE != "the wrong value"', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
+ end
+ end
+
+ context 'with an explicit `when: always`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null', when: 'always' }]],
+ [[{ if: '$VARIABLE == null', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]],
+ [[{ if: '$VARIABLE != "the wrong value"', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'always')
+ end
+ end
+ end
+
+ context 'with an explicit `when: on_failure`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$CI_JOB_NAME == "rspec" && $VAR == null', when: 'on_failure' }]],
+ [[{ if: '$VARIABLE != null', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]],
+ [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_BUILD_NAME == "rspec"', when: 'on_failure' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_failure')
+ end
+ end
+ end
+
+ context 'with an explicit `when: delayed`' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }]],
+ [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]],
+ [[{ if: '$VARIABLE != "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'delayed', start_in: '1 day')
+ end
+ end
+ end
+
+ context 'without an explicit when: value' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE == null' }]],
+ [[{ if: '$VARIABLE == null' }, { if: '$VARIABLE == null' }]],
+ [[{ if: '$VARIABLE != "the wrong value"' }, { if: '$VARIABLE == null' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_success')
+ end
+ end
+ end
+ end
+
+ context 'with a matching changes: rule' do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project).tap do |pipeline|
+ stub_pipeline_modified_paths(pipeline, %w[app/models/ci/pipeline.rb spec/models/ci/pipeline_spec.rb .gitlab-ci.yml])
+ end
+ end
+
+ context 'with an explicit `when: never`' do
+ where(:rule_set) do
+ [
+ [[{ changes: %w[*/**/*.rb], when: 'never' }, { changes: %w[*/**/*.rb], when: 'always' }]],
+ [[{ changes: %w[app/models/ci/pipeline.rb], when: 'never' }, { changes: %w[app/models/ci/pipeline.rb], when: 'always' }]],
+ [[{ changes: %w[spec/**/*.rb], when: 'never' }, { changes: %w[spec/**/*.rb], when: 'always' }]],
+ [[{ changes: %w[*.yml], when: 'never' }, { changes: %w[*.yml], when: 'always' }]],
+ [[{ changes: %w[.*.yml], when: 'never' }, { changes: %w[.*.yml], when: 'always' }]],
+ [[{ changes: %w[**/*], when: 'never' }, { changes: %w[**/*], when: 'always' }]],
+ [[{ changes: %w[*/**/*.rb *.yml], when: 'never' }, { changes: %w[*/**/*.rb *.yml], when: 'always' }]],
+ [[{ changes: %w[.*.yml **/*], when: 'never' }, { changes: %w[.*.yml **/*], when: 'always' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
+ end
+ end
+
+ context 'with an explicit `when: always`' do
+ where(:rule_set) do
+ [
+ [[{ changes: %w[*/**/*.rb], when: 'always' }, { changes: %w[*/**/*.rb], when: 'never' }]],
+ [[{ changes: %w[app/models/ci/pipeline.rb], when: 'always' }, { changes: %w[app/models/ci/pipeline.rb], when: 'never' }]],
+ [[{ changes: %w[spec/**/*.rb], when: 'always' }, { changes: %w[spec/**/*.rb], when: 'never' }]],
+ [[{ changes: %w[*.yml], when: 'always' }, { changes: %w[*.yml], when: 'never' }]],
+ [[{ changes: %w[.*.yml], when: 'always' }, { changes: %w[.*.yml], when: 'never' }]],
+ [[{ changes: %w[**/*], when: 'always' }, { changes: %w[**/*], when: 'never' }]],
+ [[{ changes: %w[*/**/*.rb *.yml], when: 'always' }, { changes: %w[*/**/*.rb *.yml], when: 'never' }]],
+ [[{ changes: %w[.*.yml **/*], when: 'always' }, { changes: %w[.*.yml **/*], when: 'never' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'always')
+ end
+ end
+ end
+
+ context 'without an explicit when: value' do
+ where(:rule_set) do
+ [
+ [[{ changes: %w[*/**/*.rb] }]],
+ [[{ changes: %w[app/models/ci/pipeline.rb] }]],
+ [[{ changes: %w[spec/**/*.rb] }]],
+ [[{ changes: %w[*.yml] }]],
+ [[{ changes: %w[.*.yml] }]],
+ [[{ changes: %w[**/*] }]],
+ [[{ changes: %w[*/**/*.rb *.yml] }]],
+ [[{ changes: %w[.*.yml **/*] }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'on_success')
+ end
+ end
+ end
+ end
+
+ context 'with no matching rule' do
+ where(:rule_set) do
+ [
+ [[{ if: '$VARIABLE != null', when: 'never' }]],
+ [[{ if: '$VARIABLE != null', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]],
+ [[{ if: '$VARIABLE == "the wrong value"', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]],
+ [[{ if: '$VARIABLE != null', when: 'always' }]],
+ [[{ if: '$VARIABLE != null', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]],
+ [[{ if: '$VARIABLE == "the wrong value"', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]],
+ [[{ if: '$VARIABLE != null' }]],
+ [[{ if: '$VARIABLE != null' }, { if: '$VARIABLE != null' }]],
+ [[{ if: '$VARIABLE == "the wrong value"' }, { if: '$VARIABLE != null' }]]
+ ]
+ end
+
+ with_them do
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
+ end
+ end
+
+ context 'with no rules' do
+ let(:rule_set) { [] }
+
+ it { is_expected.not_to be_included }
+
+ it 'correctly populates when:' do
+ expect(seed_build.attributes).to include(when: 'never')
+ end
+ end
+ end
end
describe 'applying needs: dependency' do
@@ -476,4 +755,10 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
end
+
+ describe '#scoped_variables_hash' do
+ subject { seed_build.scoped_variables_hash }
+
+ it { is_expected.to eq(seed_build.to_resource.scoped_variables_hash) }
+ end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index d5567b4f166..91c559dcd9b 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -125,9 +125,11 @@ module Gitlab
describe 'delayed job entry' do
context 'when delayed is defined' do
let(:config) do
- YAML.dump(rspec: { script: 'rollout 10%',
- when: 'delayed',
- start_in: '1 day' })
+ YAML.dump(rspec: {
+ script: 'rollout 10%',
+ when: 'delayed',
+ start_in: '1 day'
+ })
end
it 'has the attributes' do
@@ -726,12 +728,12 @@ module Gitlab
end
end
- describe "When" do
- %w(on_success on_failure always).each do |when_state|
- it "returns #{when_state} when defined" do
+ describe 'when:' do
+ (Gitlab::Ci::Config::Entry::Job::ALLOWED_WHEN - %w[delayed]).each do |when_state|
+ it "#{when_state} creates one build and sets when:" do
config = YAML.dump({
- rspec: { script: "rspec", when: when_state }
- })
+ rspec: { script: 'rspec', when: when_state }
+ })
config_processor = Gitlab::Ci::YamlProcessor.new(config)
builds = config_processor.stage_builds_attributes("test")
@@ -740,6 +742,35 @@ module Gitlab
expect(builds.first[:when]).to eq(when_state)
end
end
+
+ context 'delayed' do
+ context 'with start_in' do
+ it 'creates one build and sets when:' do
+ config = YAML.dump({
+ rspec: { script: 'rspec', when: 'delayed', start_in: '1 hour' }
+ })
+
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
+ builds = config_processor.stage_builds_attributes("test")
+
+ expect(builds.size).to eq(1)
+ expect(builds.first[:when]).to eq('delayed')
+ expect(builds.first[:options][:start_in]).to eq('1 hour')
+ end
+ end
+
+ context 'without start_in' do
+ it 'raises an error' do
+ config = YAML.dump({
+ rspec: { script: 'rspec', when: 'delayed' }
+ })
+
+ expect do
+ Gitlab::Ci::YamlProcessor.new(config)
+ end.to raise_error(YamlProcessor::ValidationError, /start in should be a duration/)
+ end
+ end
+ end
end
describe 'Parallel' do
@@ -1132,7 +1163,7 @@ module Gitlab
it { expect { subject }.not_to raise_error }
end
- context 'needs to builds' do
+ context 'needs two builds' do
let(:needs) { %w(build1 build2) }
it "does create jobs with valid specification" do
@@ -1169,7 +1200,7 @@ module Gitlab
end
end
- context 'needs to builds defined as symbols' do
+ context 'needs two builds defined as symbols' do
let(:needs) { [:build1, :build2] }
it { expect { subject }.not_to raise_error }
@@ -1195,6 +1226,67 @@ module Gitlab
end
end
+ describe 'rules' do
+ subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
+
+ let(:config) do
+ {
+ var_default: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null' }] },
+ var_when: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', when: 'always' }] },
+ var_and_changes: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', changes: %w[README], when: 'always' }] },
+ changes_not_var: { stage: 'test', script: 'test', rules: [{ if: '$VAR != null', changes: %w[README] }] },
+ var_not_changes: { stage: 'test', script: 'test', rules: [{ if: '$VAR == null', changes: %w[other/file.rb], when: 'always' }] },
+ nothing: { stage: 'test', script: 'test', rules: [{ when: 'manual' }] },
+ var_never: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'never' }] },
+ var_delayed: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] },
+ two_rules: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'on_success' }, { changes: %w[README], when: 'manual' }] }
+ }
+ end
+
+ it 'raises no exceptions' do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'returns all jobs regardless of their inclusion' do
+ expect(subject.builds.count).to eq(config.keys.count)
+ end
+
+ context 'used with job-level when' do
+ let(:config) do
+ {
+ var_default: {
+ stage: 'build',
+ script: 'test',
+ when: 'always',
+ rules: [{ if: '$VAR == null' }]
+ }
+ }
+ end
+
+ it 'raises a ValidationError' do
+ expect { subject }.to raise_error(YamlProcessor::ValidationError, /may not be used with `rules`: when/)
+ end
+ end
+
+ context 'used with job-level when:delayed' do
+ let(:config) do
+ {
+ var_default: {
+ stage: 'build',
+ script: 'test',
+ when: 'delayed',
+ start_in: '10 minutes',
+ rules: [{ if: '$VAR == null' }]
+ }
+ }
+ end
+
+ it 'raises a ValidationError' do
+ expect { subject }.to raise_error(YamlProcessor::ValidationError, /may not be used with `rules`: when, start_in/)
+ end
+ end
+ end
+
describe "Hidden jobs" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
subject { config_processor.stage_builds_attributes("test") }
@@ -1513,7 +1605,7 @@ module Gitlab
config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always, manual or delayed")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be one of: #{Gitlab::Ci::Config::Entry::Job::ALLOWED_WHEN.join(', ')}")
end
it "returns errors if job artifacts:name is not an a string" do
diff --git a/spec/lib/gitlab/daemon_spec.rb b/spec/lib/gitlab/daemon_spec.rb
index d3e73314b87..0372b770844 100644
--- a/spec/lib/gitlab/daemon_spec.rb
+++ b/spec/lib/gitlab/daemon_spec.rb
@@ -34,12 +34,12 @@ describe Gitlab::Daemon do
end
end
- describe 'when Daemon is enabled' do
+ context 'when Daemon is enabled' do
before do
allow(subject).to receive(:enabled?).and_return(true)
end
- describe 'when Daemon is stopped' do
+ context 'when Daemon is stopped' do
describe '#start' do
it 'starts the Daemon' do
expect { subject.start.join }.to change { subject.thread? }.from(false).to(true)
@@ -57,14 +57,14 @@ describe Gitlab::Daemon do
end
end
- describe 'when Daemon is running' do
+ context 'when Daemon is running' do
before do
- subject.start.join
+ subject.start
end
describe '#start' do
it "doesn't start running Daemon" do
- expect { subject.start.join }.not_to change { subject.thread? }
+ expect { subject.start.join }.not_to change { subject.thread }
expect(subject).to have_received(:start_working).once
end
@@ -76,11 +76,29 @@ describe Gitlab::Daemon do
expect(subject).to have_received(:stop_working)
end
+
+ context 'when stop_working raises exception' do
+ before do
+ allow(subject).to receive(:start_working) do
+ sleep(1000)
+ end
+ end
+
+ it 'shutdowns Daemon' do
+ expect(subject).to receive(:stop_working) do
+ subject.thread.raise(Interrupt)
+ end
+
+ expect(subject.thread).to be_alive
+ expect { subject.stop }.not_to raise_error
+ expect(subject.thread).to be_nil
+ end
+ end
end
end
end
- describe 'when Daemon is disabled' do
+ context 'when Daemon is disabled' do
before do
allow(subject).to receive(:enabled?).and_return(false)
end
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index 0295138fc3a..04fc24b6205 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::RepositoryCacheAdapter do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:cache) { repository.send(:cache) }
+ let(:redis_set_cache) { repository.send(:redis_set_cache) }
describe '#cache_method_output', :use_clean_rails_memory_store_caching do
let(:fallback) { 10 }
@@ -206,9 +207,11 @@ describe Gitlab::RepositoryCacheAdapter do
describe '#expire_method_caches' do
it 'expires the caches of the given methods' do
expect(cache).to receive(:expire).with(:rendered_readme)
- expect(cache).to receive(:expire).with(:gitignore)
+ expect(cache).to receive(:expire).with(:branch_names)
+ expect(redis_set_cache).to receive(:expire).with(:rendered_readme)
+ expect(redis_set_cache).to receive(:expire).with(:branch_names)
- repository.expire_method_caches(%i(rendered_readme gitignore))
+ repository.expire_method_caches(%i(rendered_readme branch_names))
end
it 'does not expire caches for non-existent methods' do
diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb
new file mode 100644
index 00000000000..9695e13d842
--- /dev/null
+++ b/spec/lib/gitlab/repository_set_cache_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:namespace) { "#{repository.full_path}:#{project.id}" }
+ let(:cache) { described_class.new(repository) }
+
+ describe '#cache_key' do
+ subject { cache.cache_key(:foo) }
+
+ it 'includes the namespace' do
+ is_expected.to eq("foo:#{namespace}:set")
+ end
+
+ context 'with a given namespace' do
+ let(:extra_namespace) { 'my:data' }
+ let(:cache) { described_class.new(repository, extra_namespace: extra_namespace) }
+
+ it 'includes the full namespace' do
+ is_expected.to eq("foo:#{namespace}:#{extra_namespace}:set")
+ end
+ end
+ end
+
+ describe '#expire' do
+ it 'expires the given key from the cache' do
+ cache.write(:foo, ['value'])
+
+ expect(cache.read(:foo)).to contain_exactly('value')
+ expect(cache.expire(:foo)).to eq(1)
+ expect(cache.read(:foo)).to be_empty
+ end
+ end
+
+ describe '#exist?' do
+ it 'checks whether the key exists' do
+ expect(cache.exist?(:foo)).to be(false)
+
+ cache.write(:foo, ['value'])
+
+ expect(cache.exist?(:foo)).to be(true)
+ end
+ end
+
+ describe '#fetch' do
+ let(:blk) { -> { ['block value'] } }
+
+ subject { cache.fetch(:foo, &blk) }
+
+ it 'fetches the key from the cache when filled' do
+ cache.write(:foo, ['value'])
+
+ is_expected.to contain_exactly('value')
+ end
+
+ it 'writes the value of the provided block when empty' do
+ cache.expire(:foo)
+
+ is_expected.to contain_exactly('block value')
+ expect(cache.read(:foo)).to contain_exactly('block value')
+ end
+ end
+
+ describe '#include?' do
+ it 'checks inclusion in the Redis set' do
+ cache.write(:foo, ['value'])
+
+ expect(cache.include?(:foo, 'value')).to be(true)
+ expect(cache.include?(:foo, 'bar')).to be(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
index af8b059b984..a48cc0d128a 100644
--- a/spec/lib/gitlab/sentry_spec.rb
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -65,6 +65,7 @@ describe Gitlab::Sentry do
context '.track_acceptable_exception' do
let(:exception) { RuntimeError.new('boom') }
+ let(:issue_url) { 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1' }
before do
allow(described_class).to receive(:enabled?).and_return(true)
@@ -74,7 +75,7 @@ describe Gitlab::Sentry do
it 'calls Raven.capture_exception' do
expected_extras = {
some_other_info: 'info',
- issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1'
+ issue_url: issue_url
}
expected_tags = {
@@ -88,9 +89,33 @@ describe Gitlab::Sentry do
described_class.track_acceptable_exception(
exception,
- issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1',
+ issue_url: issue_url,
extra: { some_other_info: 'info' }
)
end
+
+ context 'the exception implements :sentry_extra_data' do
+ let(:extra_info) { { event: 'explosion', size: :massive } }
+ let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info) }
+
+ it 'includes the extra data from the exception in the tracking information' do
+ expect(Raven).to receive(:capture_exception)
+ .with(exception, a_hash_including(extra: a_hash_including(extra_info)))
+
+ described_class.track_acceptable_exception(exception)
+ end
+ end
+
+ context 'the exception implements :sentry_extra_data, which returns nil' do
+ let(:exception) { double(message: 'bang!', sentry_extra_data: nil) }
+
+ it 'just includes the other extra info' do
+ extra_info = { issue_url: issue_url }
+ expect(Raven).to receive(:capture_exception)
+ .with(exception, a_hash_including(extra: a_hash_including(extra_info)))
+
+ described_class.track_acceptable_exception(exception, extra_info)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
index c6df1c6a0d8..e430599bd94 100644
--- a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::SidekiqMiddleware::Metrics do
let(:running_jobs_metric) { double('running jobs metric') }
before do
- allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything).and_return(completion_seconds_metric)
+ allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything).and_return(completion_seconds_metric)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric)
allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric)
allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :livesum).and_return(running_jobs_metric)
diff --git a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb
new file mode 100644
index 00000000000..2933d26a387
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::Monitor do
+ let(:monitor) { described_class.new }
+
+ describe '#call' do
+ let(:worker) { double }
+ let(:job) { { 'jid' => 'job-id' } }
+ let(:queue) { 'my-queue' }
+
+ it 'calls SidekiqMonitor' do
+ expect(Gitlab::SidekiqMonitor.instance).to receive(:within_job)
+ .with('job-id', 'my-queue')
+ .and_call_original
+
+ expect { |blk| monitor.call(worker, job, queue, &blk) }.to yield_control
+ end
+
+ it 'passthroughs the return value' do
+ result = monitor.call(worker, job, queue) do
+ 'value'
+ end
+
+ expect(result).to eq('value')
+ end
+
+ context 'when cancel happens' do
+ subject do
+ monitor.call(worker, job, queue) do
+ raise Gitlab::SidekiqMonitor::CancelledError
+ end
+ end
+
+ it 'does skip this job' do
+ expect { subject }.to raise_error(Sidekiq::JobRetry::Skip)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_monitor_spec.rb b/spec/lib/gitlab/sidekiq_monitor_spec.rb
new file mode 100644
index 00000000000..bbd7bf90217
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_monitor_spec.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMonitor do
+ let(:monitor) { described_class.new }
+
+ describe '#within_job' do
+ it 'tracks thread' do
+ blk = proc do
+ expect(monitor.jobs_thread['jid']).not_to be_nil
+
+ "OK"
+ end
+
+ expect(monitor.within_job('jid', 'queue', &blk)).to eq("OK")
+ end
+
+ context 'when job is canceled' do
+ let(:jid) { SecureRandom.hex }
+
+ before do
+ described_class.cancel_job(jid)
+ end
+
+ it 'does not execute a block' do
+ expect do |blk|
+ monitor.within_job(jid, 'queue', &blk)
+ rescue described_class::CancelledError
+ end.not_to yield_control
+ end
+
+ it 'raises exception' do
+ expect { monitor.within_job(jid, 'queue') }.to raise_error(
+ described_class::CancelledError)
+ end
+ end
+ end
+
+ describe '#start_working' do
+ subject { monitor.send(:start_working) }
+
+ before do
+ # we want to run at most once cycle
+ # we toggle `enabled?` flag after the first call
+ stub_const('Gitlab::SidekiqMonitor::RECONNECT_TIME', 0)
+ allow(monitor).to receive(:enabled?).and_return(true, false)
+
+ allow(Sidekiq.logger).to receive(:info)
+ allow(Sidekiq.logger).to receive(:warn)
+ end
+
+ context 'when structured logging is used' do
+ it 'logs start message' do
+ expect(Sidekiq.logger).to receive(:info)
+ .with(
+ class: described_class.to_s,
+ action: 'start',
+ message: 'Starting Monitor Daemon')
+
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+
+ subject
+ end
+
+ it 'logs stop message' do
+ expect(Sidekiq.logger).to receive(:warn)
+ .with(
+ class: described_class.to_s,
+ action: 'stop',
+ message: 'Stopping Monitor Daemon')
+
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+
+ subject
+ end
+
+ it 'logs StandardError message' do
+ expect(Sidekiq.logger).to receive(:warn)
+ .with(
+ class: described_class.to_s,
+ action: 'exception',
+ message: 'My Exception')
+
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+ .and_raise(StandardError, 'My Exception')
+
+ expect { subject }.not_to raise_error
+ end
+
+ it 'logs and raises Exception message' do
+ expect(Sidekiq.logger).to receive(:warn)
+ .with(
+ class: described_class.to_s,
+ action: 'exception',
+ message: 'My Exception')
+
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+ .and_raise(Exception, 'My Exception')
+
+ expect { subject }.to raise_error(Exception, 'My Exception')
+ end
+ end
+
+ context 'when StandardError is raised' do
+ it 'does retry connection' do
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+ .and_raise(StandardError, 'My Exception')
+
+ expect(::Gitlab::Redis::SharedState).to receive(:with)
+
+ # we expect to run `process_messages` twice
+ expect(monitor).to receive(:enabled?).and_return(true, true, false)
+
+ subject
+ end
+ end
+
+ context 'when message is published' do
+ let(:subscribed) { double }
+
+ before do
+ expect_any_instance_of(::Redis).to receive(:subscribe)
+ .and_yield(subscribed)
+
+ expect(subscribed).to receive(:message)
+ .and_yield(
+ described_class::NOTIFICATION_CHANNEL,
+ payload
+ )
+
+ expect(Sidekiq.logger).to receive(:info)
+ .with(
+ class: described_class.to_s,
+ action: 'start',
+ message: 'Starting Monitor Daemon')
+
+ expect(Sidekiq.logger).to receive(:info)
+ .with(
+ class: described_class.to_s,
+ channel: described_class::NOTIFICATION_CHANNEL,
+ message: 'Received payload on channel',
+ payload: payload
+ )
+ end
+
+ context 'and message is valid' do
+ let(:payload) { '{"action":"cancel","jid":"my-jid"}' }
+
+ it 'processes cancel' do
+ expect(monitor).to receive(:process_job_cancel).with('my-jid')
+
+ subject
+ end
+ end
+
+ context 'and message is not valid json' do
+ let(:payload) { '{"action"}' }
+
+ it 'skips processing' do
+ expect(monitor).not_to receive(:process_job_cancel)
+
+ subject
+ end
+ end
+ end
+ end
+
+ describe '#stop' do
+ let!(:monitor_thread) { monitor.start }
+
+ it 'does stop the thread' do
+ expect(monitor_thread).to be_alive
+
+ expect { monitor.stop }.not_to raise_error
+
+ expect(monitor_thread).not_to be_alive
+ expect { monitor_thread.value }.to raise_error(Interrupt)
+ end
+ end
+
+ describe '#process_job_cancel' do
+ subject { monitor.send(:process_job_cancel, jid) }
+
+ context 'when jid is missing' do
+ let(:jid) { nil }
+
+ it 'does not run thread' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when jid is provided' do
+ let(:jid) { 'my-jid' }
+
+ context 'when jid is not found' do
+ it 'does not log cancellation message' do
+ expect(Sidekiq.logger).not_to receive(:warn)
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when jid is found' do
+ let(:thread) { Thread.new { sleep 1000 } }
+
+ before do
+ monitor.jobs_thread[jid] = thread
+ end
+
+ after do
+ thread.kill
+ rescue
+ end
+
+ it 'does log cancellation message' do
+ expect(Sidekiq.logger).to receive(:warn)
+ .with(
+ class: described_class.to_s,
+ action: 'cancel',
+ message: 'Canceling thread with CancelledError',
+ jid: 'my-jid',
+ thread_id: thread.object_id)
+
+ expect(subject).to be_a(Thread)
+
+ subject.join
+ end
+
+ it 'does cancel the thread' do
+ expect(subject).to be_a(Thread)
+
+ subject.join
+
+ # we wait for the thread to be cancelled
+ # by `process_job_cancel`
+ expect { thread.join(5) }.to raise_error(described_class::CancelledError)
+ end
+ end
+ end
+ end
+
+ describe '.cancel_job' do
+ subject { described_class.cancel_job('my-jid') }
+
+ it 'sets a redis key' do
+ expect_any_instance_of(::Redis).to receive(:setex)
+ .with('sidekiq:cancel:my-jid', anything, 1)
+
+ subject
+ end
+
+ it 'notifies all workers' do
+ payload = '{"action":"cancel","jid":"my-jid"}'
+
+ expect_any_instance_of(::Redis).to receive(:publish)
+ .with('sidekiq:cancel:notifications', payload)
+
+ subject
+ end
+ end
+end
diff --git a/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb
index 1669a22879f..b385d1b07c7 100644
--- a/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb
@@ -26,16 +26,22 @@ describe Gitlab::UsageDataCounters::NoteCounter, :clean_gitlab_redis_shared_stat
end
it_behaves_like 'a note usage counter', :create, 'Snippet'
+ it_behaves_like 'a note usage counter', :create, 'MergeRequest'
+ it_behaves_like 'a note usage counter', :create, 'Commit'
describe '.totals' do
let(:combinations) do
[
- [:create, 'Snippet', 3]
+ [:create, 'Snippet', 3],
+ [:create, 'MergeRequest', 4],
+ [:create, 'Commit', 5]
]
end
let(:expected_totals) do
- { snippet_comment: 3 }
+ { snippet_comment: 3,
+ merge_request_comment: 4,
+ commit_comment: 5 }
end
before do
@@ -57,14 +63,18 @@ describe Gitlab::UsageDataCounters::NoteCounter, :clean_gitlab_redis_shared_stat
let(:unknown_event_error) { Gitlab::UsageDataCounters::BaseCounter::UnknownEvent }
where(:event, :noteable_type, :expected_count, :should_raise) do
- :create | 'Snippet' | 1 | false
- :wibble | 'Snippet' | 0 | true
- :create | 'Issue' | 0 | false
- :wibble | 'Issue' | 0 | false
+ :create | 'Snippet' | 1 | false
+ :wibble | 'Snippet' | 0 | true
+ :create | 'MergeRequest' | 1 | false
+ :wibble | 'MergeRequest' | 0 | true
+ :create | 'Commit' | 1 | false
+ :wibble | 'Commit' | 0 | true
+ :create | 'Issue' | 0 | false
+ :wibble | 'Issue' | 0 | false
end
with_them do
- it "handles event" do
+ it 'handles event' do
if should_raise
expect { described_class.count(event, noteable_type) }.to raise_error(unknown_event_error)
else
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 64254674157..dda119e09b1 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::UsageData do
@@ -34,7 +36,7 @@ describe Gitlab::UsageData do
subject { described_class.data }
- it "gathers usage data" do
+ it 'gathers usage data' do
expect(subject.keys).to include(*%i(
active_user_count
counts
@@ -66,6 +68,8 @@ describe Gitlab::UsageData do
snippet_create: a_kind_of(Integer),
snippet_update: a_kind_of(Integer),
snippet_comment: a_kind_of(Integer),
+ merge_request_comment: a_kind_of(Integer),
+ commit_comment: a_kind_of(Integer),
wiki_pages_create: a_kind_of(Integer),
wiki_pages_update: a_kind_of(Integer),
wiki_pages_delete: a_kind_of(Integer),
@@ -78,7 +82,7 @@ describe Gitlab::UsageData do
)
end
- it "gathers usage counts" do
+ it 'gathers usage counts' do
expected_keys = %i(
assignee_lists
boards
@@ -248,7 +252,7 @@ describe Gitlab::UsageData do
describe '#license_usage_data' do
subject { described_class.license_usage_data }
- it "gathers license data" do
+ it 'gathers license data' do
expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid)
expect(subject[:version]).to eq(Gitlab::VERSION)
expect(subject[:installation_type]).to eq('gitlab-development-kit')
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index dcc4b70a382..6cba7df114c 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -543,6 +543,73 @@ describe Notify do
end
end
+ describe '#mail_thread' do
+ set(:mail_thread_note) { create(:note) }
+
+ let(:headers) do
+ {
+ from: 'someone@test.com',
+ to: 'someone-else@test.com',
+ subject: 'something',
+ template_name: '_note_email' # re-use this for testing
+ }
+ end
+
+ let(:mailer) do
+ mailer = described_class.new
+ mailer.instance_variable_set(:@note, mail_thread_note)
+ mailer
+ end
+
+ context 'the model has no namespace' do
+ class TopLevelThing
+ include Referable
+ include Noteable
+
+ def to_reference(*_args)
+ 'tlt-ref'
+ end
+
+ def id
+ 'tlt-id'
+ end
+ end
+
+ subject do
+ mailer.send(:mail_thread, TopLevelThing.new, headers)
+ end
+
+ it 'has X-GitLab-Namespaced-Thing-ID header' do
+ expect(subject.header['X-GitLab-TopLevelThing-ID'].value).to eq('tlt-id')
+ end
+ end
+
+ context 'the model has a namespace' do
+ module Namespaced
+ class Thing
+ include Referable
+ include Noteable
+
+ def to_reference(*_args)
+ 'some-reference'
+ end
+
+ def id
+ 'some-id'
+ end
+ end
+ end
+
+ subject do
+ mailer.send(:mail_thread, Namespaced::Thing.new, headers)
+ end
+
+ it 'has X-GitLab-Namespaced-Thing-ID header' do
+ expect(subject.header['X-GitLab-Namespaced-Thing-ID'].value).to eq('some-id')
+ end
+ end
+ end
+
context 'for issue notes' do
let(:host) { Gitlab.config.gitlab.host }
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
index 8452ac69734..b15b26b1630 100644
--- a/spec/models/award_emoji_spec.rb
+++ b/spec/models/award_emoji_spec.rb
@@ -44,6 +44,29 @@ describe AwardEmoji do
end
end
+ describe 'scopes' do
+ set(:thumbsup) { create(:award_emoji, name: 'thumbsup') }
+ set(:thumbsdown) { create(:award_emoji, name: 'thumbsdown') }
+
+ describe '.upvotes' do
+ it { expect(described_class.upvotes).to contain_exactly(thumbsup) }
+ end
+
+ describe '.downvotes' do
+ it { expect(described_class.downvotes).to contain_exactly(thumbsdown) }
+ end
+
+ describe '.named' do
+ it { expect(described_class.named('thumbsup')).to contain_exactly(thumbsup) }
+ it { expect(described_class.named(%w[thumbsup thumbsdown])).to contain_exactly(thumbsup, thumbsdown) }
+ end
+
+ describe '.awarded_by' do
+ it { expect(described_class.awarded_by(thumbsup.user)).to contain_exactly(thumbsup) }
+ it { expect(described_class.awarded_by([thumbsup.user, thumbsdown.user])).to contain_exactly(thumbsup, thumbsdown) }
+ end
+ end
+
describe 'expiring ETag cache' do
context 'on a note' do
let(:note) { create(:note_on_issue) }
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index 9e7106281ee..76da42cf243 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -82,16 +82,6 @@ describe Awardable do
end
end
- describe "#toggle_award_emoji" do
- it "adds an emoji if it isn't awarded yet" do
- expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1)
- end
-
- it "toggles already awarded emoji" do
- expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1)
- end
- end
-
describe 'querying award_emoji on an Awardable' do
let(:issue) { create(:issue) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 419e1dc2459..79395fcc994 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1223,36 +1223,66 @@ describe Repository do
end
describe '#branch_exists?' do
- it 'uses branch_names' do
- allow(repository).to receive(:branch_names).and_return(['foobar'])
+ let(:branch) { repository.root_ref }
- expect(repository.branch_exists?('foobar')).to eq(true)
- expect(repository.branch_exists?('master')).to eq(false)
+ subject { repository.branch_exists?(branch) }
+
+ it 'delegates to branch_names when the cache is empty' do
+ repository.expire_branches_cache
+
+ expect(repository).to receive(:branch_names).and_call_original
+ is_expected.to eq(true)
+ end
+
+ it 'uses redis set caching when the cache is filled' do
+ repository.branch_names # ensure the branch name cache is filled
+
+ expect(repository)
+ .to receive(:branch_names_include?)
+ .with(branch)
+ .and_call_original
+
+ is_expected.to eq(true)
end
end
describe '#tag_exists?' do
- it 'uses tag_names' do
- allow(repository).to receive(:tag_names).and_return(['foobar'])
+ let(:tag) { repository.tags.first.name }
+
+ subject { repository.tag_exists?(tag) }
+
+ it 'delegates to tag_names when the cache is empty' do
+ repository.expire_tags_cache
+
+ expect(repository).to receive(:tag_names).and_call_original
+ is_expected.to eq(true)
+ end
+
+ it 'uses redis set caching when the cache is filled' do
+ repository.tag_names # ensure the tag name cache is filled
+
+ expect(repository)
+ .to receive(:tag_names_include?)
+ .with(tag)
+ .and_call_original
- expect(repository.tag_exists?('foobar')).to eq(true)
- expect(repository.tag_exists?('master')).to eq(false)
+ is_expected.to eq(true)
end
end
- describe '#branch_names', :use_clean_rails_memory_store_caching do
+ describe '#branch_names', :clean_gitlab_redis_cache do
let(:fake_branch_names) { ['foobar'] }
it 'gets cached across Repository instances' do
allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names)
- expect(repository.branch_names).to eq(fake_branch_names)
+ expect(repository.branch_names).to match_array(fake_branch_names)
fresh_repository = Project.find(project.id).repository
expect(fresh_repository.object_id).not_to eq(repository.object_id)
expect(fresh_repository.raw_repository).not_to receive(:branch_names)
- expect(fresh_repository.branch_names).to eq(fake_branch_names)
+ expect(fresh_repository.branch_names).to match_array(fake_branch_names)
end
end
diff --git a/spec/presenters/blobs/unfold_presenter_spec.rb b/spec/presenters/blobs/unfold_presenter_spec.rb
index ab3f8080257..83004809536 100644
--- a/spec/presenters/blobs/unfold_presenter_spec.rb
+++ b/spec/presenters/blobs/unfold_presenter_spec.rb
@@ -10,16 +10,31 @@ describe Blobs::UnfoldPresenter do
let(:subject) { described_class.new(blob, params) }
describe '#initialize' do
+ let(:result) { subject }
+
+ context 'with empty params' do
+ let(:params) { {} }
+
+ it 'sets default attributes' do
+ expect(result.full?).to eq(false)
+ expect(result.since).to eq(1)
+ expect(result.to).to eq(1)
+ expect(result.bottom).to eq(false)
+ expect(result.unfold).to eq(true)
+ expect(result.offset).to eq(0)
+ expect(result.indent).to eq(0)
+ end
+ end
+
context 'when full is false' do
let(:params) { { full: false, since: 2, to: 3, bottom: false, offset: 1, indent: 1 } }
it 'sets attributes' do
- result = subject
-
expect(result.full?).to eq(false)
expect(result.since).to eq(2)
expect(result.to).to eq(3)
expect(result.bottom).to eq(false)
+ expect(result.unfold).to eq(true)
expect(result.offset).to eq(1)
expect(result.indent).to eq(1)
end
@@ -29,12 +44,11 @@ describe Blobs::UnfoldPresenter do
let(:params) { { full: true, since: 2, to: 3, bottom: false, offset: 1, indent: 1 } }
it 'sets other attributes' do
- result = subject
-
expect(result.full?).to eq(true)
expect(result.since).to eq(1)
expect(result.to).to eq(blob.lines.size)
expect(result.bottom).to eq(false)
+ expect(result.unfold).to eq(false)
expect(result.offset).to eq(0)
expect(result.indent).to eq(0)
end
@@ -44,12 +58,11 @@ describe Blobs::UnfoldPresenter do
let(:params) { { full: false, since: 2, to: -1, bottom: true, offset: 1, indent: 1 } }
it 'sets other attributes' do
- result = subject
-
expect(result.full?).to eq(false)
expect(result.since).to eq(2)
expect(result.to).to eq(blob.lines.size)
expect(result.bottom).to eq(false)
+ expect(result.unfold).to eq(false)
expect(result.offset).to eq(0)
expect(result.indent).to eq(0)
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 6c67d84b59b..342fcfa1041 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -155,6 +155,14 @@ describe API::AwardEmoji do
expect(json_response['user']['username']).to eq(user.username)
end
+ it 'marks Todos on the Issue as done' do
+ todo = create(:todo, target: issue, project: project, user: user)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), params: { name: '8ball' }
+
+ expect(todo.reload).to be_done
+ end
+
it "returns a 400 bad request error if the name is not given" do
post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user)
@@ -209,6 +217,14 @@ describe API::AwardEmoji do
expect(json_response['user']['username']).to eq(user.username)
end
+ it 'marks Todos on the Noteable as done' do
+ todo = create(:todo, target: note2.noteable, project: project, user: user)
+
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: 'rocket' }
+
+ expect(todo.reload).to be_done
+ end
+
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: '+1' }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
index 3982125a38a..5b910d5bfe0 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
describe 'Adding an AwardEmoji' do
include GraphqlHelpers
- let(:current_user) { create(:user) }
- let(:awardable) { create(:note) }
- let(:project) { awardable.project }
+ set(:current_user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:awardable) { create(:note, project: project) }
let(:emoji_name) { 'thumbsup' }
let(:mutation) do
variables = {
@@ -43,7 +43,7 @@ describe 'Adding an AwardEmoji' do
end
context 'when the given awardable is not an Awardable' do
- let(:awardable) { create(:label) }
+ let(:awardable) { create(:label, project: project) }
it_behaves_like 'a mutation that does not create an AwardEmoji'
@@ -52,7 +52,7 @@ describe 'Adding an AwardEmoji' do
end
context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do
- let(:awardable) { create(:system_note) }
+ let(:awardable) { create(:system_note, project: project) }
it_behaves_like 'a mutation that does not create an AwardEmoji'
@@ -73,6 +73,13 @@ describe 'Adding an AwardEmoji' do
expect(mutation_response['awardEmoji']['name']).to eq(emoji_name)
end
+ describe 'marking Todos as done' do
+ let(:user) { current_user}
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ include_examples 'creating award emojis marks Todos as done'
+ end
+
context 'when there were active record validation errors' do
before do
expect_next_instance_of(AwardEmoji) do |award|
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index 31145730f10..ae628d3e56c 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -5,9 +5,9 @@ require 'spec_helper'
describe 'Toggling an AwardEmoji' do
include GraphqlHelpers
- let(:current_user) { create(:user) }
- let(:awardable) { create(:note) }
- let(:project) { awardable.project }
+ set(:current_user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:awardable) { create(:note, project: project) }
let(:emoji_name) { 'thumbsup' }
let(:mutation) do
variables = {
@@ -40,7 +40,7 @@ describe 'Toggling an AwardEmoji' do
end
context 'when the given awardable is not an Awardable' do
- let(:awardable) { create(:label) }
+ let(:awardable) { create(:label, project: project) }
it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
@@ -49,7 +49,7 @@ describe 'Toggling an AwardEmoji' do
end
context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do
- let(:awardable) { create(:system_note) }
+ let(:awardable) { create(:system_note, project: project) }
it_behaves_like 'a mutation that does not create or destroy an AwardEmoji'
@@ -81,6 +81,13 @@ describe 'Toggling an AwardEmoji' do
expect(mutation_response['toggledOn']).to eq(true)
end
+ describe 'marking Todos as done' do
+ let(:user) { current_user}
+ subject { post_graphql_mutation(mutation, current_user: user) }
+
+ include_examples 'creating award emojis marks Todos as done'
+ end
+
context 'when there were active record validation errors' do
before do
expect_next_instance_of(AwardEmoji) do |award|
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index 5916bb11516..c487471e4a1 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -342,6 +342,14 @@ describe API::Issues do
group_project.add_reporter(user)
end
+ it 'exposes known attributes' do
+ get api(base_url, admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.last.keys).to include(*%w(id iid project_id title description))
+ expect(json_response.last).not_to have_key('subscribed')
+ end
+
it 'returns all group issues (including opened and closed)' do
get api(base_url, admin)
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
index f7ca6fd1e0a..521d6b88734 100644
--- a/spec/requests/api/issues/get_project_issues_spec.rb
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -575,6 +575,7 @@ describe API::Issues do
expect(json_response['assignee']).to be_a Hash
expect(json_response['author']).to be_a Hash
expect(json_response['confidential']).to be_falsy
+ expect(json_response['subscribed']).to be_truthy
end
it 'exposes the closed_at attribute' do
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index d195f54be11..27cf66629fe 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -216,6 +216,10 @@ describe API::Issues do
expect_paginated_array_response([issue.id, closed_issue.id])
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.last).to have_key('web_url')
+ # Calculating the value of subscribed field triggers Markdown
+ # processing. We can't do that for multiple issues / merge
+ # requests in a single API request.
+ expect(json_response.last).not_to have_key('subscribed')
end
it 'returns an array of closed issues' do
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index d832963292c..478f09a7881 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -112,9 +112,9 @@ describe 'Rack Attack global throttles' do
arguments = {
message: 'Rack_Attack',
env: :throttle,
- ip: '127.0.0.1',
+ remote_ip: '127.0.0.1',
request_method: 'GET',
- fullpath: get_args.first,
+ path: get_args.first,
user_id: user.id,
username: user.username
}
@@ -213,9 +213,9 @@ describe 'Rack Attack global throttles' do
arguments = {
message: 'Rack_Attack',
env: :throttle,
- ip: '127.0.0.1',
+ remote_ip: '127.0.0.1',
request_method: 'GET',
- fullpath: '/users/sign_in'
+ path: '/users/sign_in'
}
expect(Gitlab::AuthLogger).to receive(:error).with(arguments)
@@ -377,9 +377,9 @@ describe 'Rack Attack global throttles' do
arguments = {
message: 'Rack_Attack',
env: :throttle,
- ip: '127.0.0.1',
+ remote_ip: '127.0.0.1',
request_method: 'GET',
- fullpath: '/dashboard/snippets',
+ path: '/dashboard/snippets',
user_id: user.id,
username: user.username
}
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index c0ea2b3c389..1b19eac9a97 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -32,8 +32,8 @@ describe DeploymentEntity do
expect(subject).to include(:created_at)
end
- it 'exposes finished_at' do
- expect(subject).to include(:finished_at)
+ it 'exposes deployed_at' do
+ expect(subject).to include(:deployed_at)
end
context 'when the pipeline has another manual action' do
diff --git a/spec/serializers/merge_request_sidebar_basic_entity_spec.rb b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb
new file mode 100644
index 00000000000..b364b1a3306
--- /dev/null
+++ b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequestSidebarBasicEntity do
+ let(:project) { create :project, :repository }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ let(:request) { double('request', current_user: user, project: project) }
+
+ let(:entity) { described_class.new(merge_request, request: request).as_json }
+
+ describe '#current_user' do
+ it 'contains attributes related to the current user' do
+ expect(entity[:current_user].keys).to contain_exactly(
+ :id, :name, :username, :state, :avatar_url, :web_url, :todo,
+ :can_edit, :can_move, :can_admin_label, :can_merge
+ )
+ end
+ end
+end
diff --git a/spec/services/award_emojis/add_service_spec.rb b/spec/services/award_emojis/add_service_spec.rb
new file mode 100644
index 00000000000..037db39ba80
--- /dev/null
+++ b/spec/services/award_emojis/add_service_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardEmojis::AddService do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:awardable) { create(:note, project: project) }
+ let(:name) { 'thumbsup' }
+ subject(:service) { described_class.new(awardable, name, user) }
+
+ describe '#execute' do
+ context 'when user is not authorized' do
+ it 'does not add an emoji' do
+ expect { service.execute }.not_to change { AwardEmoji.count }
+ end
+
+ it 'returns an error state' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(:forbidden)
+ end
+ end
+
+ context 'when user is authorized' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'creates an award emoji' do
+ expect { service.execute }.to change { AwardEmoji.count }.by(1)
+ end
+
+ it 'returns the award emoji' do
+ result = service.execute
+
+ expect(result[:award]).to be_kind_of(AwardEmoji)
+ end
+
+ it 'return a success status' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:success)
+ end
+
+ it 'sets the correct properties on the award emoji' do
+ award = service.execute[:award]
+
+ expect(award.name).to eq(name)
+ expect(award.user).to eq(user)
+ end
+
+ describe 'marking Todos as done' do
+ subject { service.execute }
+
+ include_examples 'creating award emojis marks Todos as done'
+ end
+
+ context 'when the awardable cannot have emoji awarded to it' do
+ before do
+ expect(awardable).to receive(:emoji_awardable?).and_return(false)
+ end
+
+ it 'does not add an emoji' do
+ expect { service.execute }.not_to change { AwardEmoji.count }
+ end
+
+ it 'returns an error status' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(:unprocessable_entity)
+ end
+ end
+
+ context 'when the awardable is invalid' do
+ before do
+ expect_next_instance_of(AwardEmoji) do |award|
+ expect(award).to receive(:valid?).and_return(false)
+ expect(award).to receive_message_chain(:errors, :full_messages).and_return(['Error 1', 'Error 2'])
+ end
+ end
+
+ it 'does not add an emoji' do
+ expect { service.execute }.not_to change { AwardEmoji.count }
+ end
+
+ it 'returns an error status' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ end
+
+ it 'returns an error message' do
+ result = service.execute
+
+ expect(result[:message]).to eq('Error 1 and Error 2')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/awarded_emoji_finder_spec.rb b/spec/services/award_emojis/collect_user_emoji_service_spec.rb
index d4479df7418..a0dea31b403 100644
--- a/spec/finders/awarded_emoji_finder_spec.rb
+++ b/spec/services/award_emojis/collect_user_emoji_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe AwardedEmojiFinder do
+describe AwardEmojis::CollectUserEmojiService do
describe '#execute' do
it 'returns an Array containing the awarded emoji names' do
user = create(:user)
diff --git a/spec/services/award_emojis/destroy_service_spec.rb b/spec/services/award_emojis/destroy_service_spec.rb
new file mode 100644
index 00000000000..c4a7d5ec20e
--- /dev/null
+++ b/spec/services/award_emojis/destroy_service_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardEmojis::DestroyService do
+ set(:user) { create(:user) }
+ set(:awardable) { create(:note) }
+ set(:project) { awardable.project }
+ let(:name) { 'thumbsup' }
+ let!(:award_from_other_user) do
+ create(:award_emoji, name: name, awardable: awardable, user: create(:user))
+ end
+ subject(:service) { described_class.new(awardable, name, user) }
+
+ describe '#execute' do
+ shared_examples_for 'a service that does not authorize the user' do |error:|
+ it 'does not remove the emoji' do
+ expect { service.execute }.not_to change { AwardEmoji.count }
+ end
+
+ it 'returns an error state' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(:forbidden)
+ end
+
+ it 'returns a nil award' do
+ result = service.execute
+
+ expect(result).to have_key(:award)
+ expect(result[:award]).to be_nil
+ end
+
+ it 'returns the error' do
+ result = service.execute
+
+ expect(result[:message]).to eq(error)
+ expect(result[:errors]).to eq([error])
+ end
+ end
+
+ context 'when user is not authorized' do
+ it_behaves_like 'a service that does not authorize the user',
+ error: 'User cannot destroy emoji on the awardable'
+ end
+
+ context 'when the user is authorized' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when user has not awarded an emoji to the awardable' do
+ let!(:award_from_user) { create(:award_emoji, name: name, user: user) }
+
+ it_behaves_like 'a service that does not authorize the user',
+ error: 'User has not awarded emoji of type thumbsup on the awardable'
+ end
+
+ context 'when user has awarded an emoji to the awardable' do
+ let!(:award_from_user) { create(:award_emoji, name: name, awardable: awardable, user: user) }
+
+ it 'removes the emoji' do
+ expect { service.execute }.to change { AwardEmoji.count }.by(-1)
+ end
+
+ it 'returns a success status' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:success)
+ end
+
+ it 'returns no errors' do
+ result = service.execute
+
+ expect(result).not_to have_key(:error)
+ expect(result).not_to have_key(:errors)
+ end
+
+ it 'returns the destroyed award' do
+ result = service.execute
+
+ expect(result[:award]).to eq(award_from_user)
+ expect(result[:award]).to be_destroyed
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/award_emojis/toggle_service_spec.rb b/spec/services/award_emojis/toggle_service_spec.rb
new file mode 100644
index 00000000000..972a1d5fc06
--- /dev/null
+++ b/spec/services/award_emojis/toggle_service_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardEmojis::ToggleService do
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :public) }
+ set(:awardable) { create(:note, project: project) }
+ let(:name) { 'thumbsup' }
+ subject(:service) { described_class.new(awardable, name, user) }
+
+ describe '#execute' do
+ context 'when user has awarded an emoji' do
+ let!(:award_from_other_user) { create(:award_emoji, name: name, awardable: awardable, user: create(:user)) }
+ let!(:award) { create(:award_emoji, name: name, awardable: awardable, user: user) }
+
+ it 'calls AwardEmojis::DestroyService' do
+ expect(AwardEmojis::AddService).not_to receive(:new)
+
+ expect_next_instance_of(AwardEmojis::DestroyService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ service.execute
+ end
+
+ it 'destroys an AwardEmoji' do
+ expect { service.execute }.to change { AwardEmoji.count }.by(-1)
+ end
+
+ it 'returns the result of DestroyService#execute' do
+ mock_result = double(foo: true)
+
+ expect_next_instance_of(AwardEmojis::DestroyService) do |service|
+ expect(service).to receive(:execute).and_return(mock_result)
+ end
+
+ result = service.execute
+
+ expect(result).to eq(mock_result)
+ end
+ end
+
+ context 'when user has not awarded an emoji' do
+ it 'calls AwardEmojis::AddService' do
+ expect_next_instance_of(AwardEmojis::AddService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ expect(AwardEmojis::DestroyService).not_to receive(:new)
+
+ service.execute
+ end
+
+ it 'creates an AwardEmoji' do
+ expect { service.execute }.to change { AwardEmoji.count }.by(1)
+ end
+
+ it 'returns the result of AddService#execute' do
+ mock_result = double(foo: true)
+
+ expect_next_instance_of(AwardEmojis::AddService) do |service|
+ expect(service).to receive(:execute).and_return(mock_result)
+ end
+
+ result = service.execute
+
+ expect(result).to eq(mock_result)
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index d9f35afee06..fd9a63b79cc 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -229,10 +229,10 @@ describe Issues::UpdateService, :mailer do
it 'creates zoom_link_added system note when a zoom link is added to the description' do
update_issue(description: 'Changed description https://zoom.us/j/5873603787')
- note = find_note('a Zoom call was added')
+ note = find_note('added a Zoom call')
expect(note).not_to be_nil
- expect(note.note).to eq('a Zoom call was added to this issue')
+ expect(note.note).to eq('added a Zoom call to this issue')
end
context 'when issue turns confidential' do
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index b0b74407812..d4fa62fa85d 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -78,6 +78,7 @@ describe Projects::CreateService, '#execute' do
expect(project).to be_valid
expect(project.owner).to eq(group)
expect(project.namespace).to eq(group)
+ expect(project.team.owners).to include(user)
expect(user.authorized_projects).to include(project)
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 486d0ca0c56..f46f9633c1c 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -521,7 +521,7 @@ describe SystemNoteService do
end
it 'sets the zoom link added note text' do
- expect(subject.note).to eq('a Zoom call was added to this issue')
+ expect(subject.note).to eq('added a Zoom call to this issue')
end
end
@@ -533,7 +533,7 @@ describe SystemNoteService do
end
it 'sets the zoom link removed note text' do
- expect(subject.note).to eq('a Zoom call was removed from this issue')
+ expect(subject.note).to eq('removed a Zoom call from this issue')
end
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index badea94352a..7d10cffe920 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -22,6 +22,10 @@ module StubGitlabCalls
allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml }
end
+ def stub_pipeline_modified_paths(pipeline, modified_paths)
+ allow(pipeline).to receive(:modified_paths).and_return(modified_paths)
+ end
+
def stub_repository_ci_yaml_file(sha:, path: '.gitlab-ci.yml')
allow_any_instance_of(Repository)
.to receive(:gitlab_ci_yml_for).with(sha, path)
diff --git a/spec/support/shared_examples/award_emoji_todo_shared_examples.rb b/spec/support/shared_examples/award_emoji_todo_shared_examples.rb
new file mode 100644
index 00000000000..88ad37d232f
--- /dev/null
+++ b/spec/support/shared_examples/award_emoji_todo_shared_examples.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+# Shared examples to that test code that creates AwardEmoji also mark Todos
+# as done.
+#
+# The examples expect these to be defined in the calling spec:
+# - `subject` the callable code that executes the creation of an AwardEmoji
+# - `user`
+# - `project`
+RSpec.shared_examples 'creating award emojis marks Todos as done' do
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ project.add_developer(user)
+ end
+
+ where(:type, :expectation) do
+ :issue | true
+ :merge_request | true
+ :project_snippet | false
+ end
+
+ with_them do
+ let(:project) { awardable.project }
+ let(:awardable) { create(type) }
+ let!(:todo) { create(:todo, target: awardable, project: project, user: user) }
+
+ it do
+ subject
+
+ expect(todo.reload.done?).to eq(expectation)
+ end
+ end
+
+ # Notes have more complicated rules than other Todoables
+ describe 'for notes' do
+ let!(:todo) { create(:todo, target: awardable.noteable, project: project, user: user) }
+
+ context 'regular Notes' do
+ let(:awardable) { create(:note, project: project) }
+
+ it 'marks the Todo as done' do
+ subject
+
+ expect(todo.reload.done?).to eq(true)
+ end
+ end
+
+ context 'PersonalSnippet Notes' do
+ let(:awardable) { create(:note, noteable: create(:personal_snippet, author: user)) }
+
+ it 'does not mark the Todo as done' do
+ subject
+
+ expect(todo.reload.done?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb
index 2faa0cf8c1c..d8a1ae83f61 100644
--- a/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb
@@ -8,7 +8,7 @@ shared_examples 'disabled when using an external authorization service' do
it 'works when the feature is not enabled' do
subject
- expect(response).to be_success
+ expect(response).to be_successful
end
it 'renders a 404 with a message when the feature is enabled' do
diff --git a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
index 1cd14ea2251..d89eded6e69 100644
--- a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
@@ -2,14 +2,14 @@
shared_examples 'set sort order from user preference' do
describe '#set_sort_order_from_user_preference' do
- # There is no issuable_sorting_field defined in any CE controllers yet,
+ # There is no sorting_field defined in any CE controllers yet,
# however any other field present in user_preferences table can be used for testing.
context 'when database is in read-only mode' do
it 'does not update user preference' do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
- expect_any_instance_of(UserPreference).not_to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param })
+ expect_any_instance_of(UserPreference).not_to receive(:update).with({ controller.send(:sorting_field) => sorting_param })
get :index, params: { namespace_id: project.namespace, project_id: project, sort: sorting_param }
end
@@ -19,7 +19,7 @@ shared_examples 'set sort order from user preference' do
it 'updates user preference' do
allow(Gitlab::Database).to receive(:read_only?).and_return(false)
- expect_any_instance_of(UserPreference).to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param })
+ expect_any_instance_of(UserPreference).to receive(:update).with({ controller.send(:sorting_field) => sorting_param })
get :index, params: { namespace_id: project.namespace, project_id: project, sort: sorting_param }
end
diff --git a/yarn.lock b/yarn.lock
index a295039ec54..525c77e41d2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9,7 +9,7 @@
dependencies:
"@babel/highlight" "^7.0.0"
-"@babel/core@>=7.2.2", "@babel/core@^7.1.0", "@babel/core@^7.1.2", "@babel/core@^7.4.4":
+"@babel/core@>=7.2.2", "@babel/core@^7.1.0", "@babel/core@^7.1.2", "@babel/core@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30"
integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg==
@@ -139,16 +139,16 @@
"@babel/types" "^7.0.0"
"@babel/helper-module-transforms@^7.1.0", "@babel/helper-module-transforms@^7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz#96115ea42a2f139e619e98ed46df6019b94414b8"
- integrity sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w==
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a"
+ integrity sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw==
dependencies:
"@babel/helper-module-imports" "^7.0.0"
"@babel/helper-simple-access" "^7.1.0"
"@babel/helper-split-export-declaration" "^7.4.4"
"@babel/template" "^7.4.4"
- "@babel/types" "^7.4.4"
- lodash "^4.17.11"
+ "@babel/types" "^7.5.5"
+ lodash "^4.17.13"
"@babel/helper-optimise-call-expression@^7.0.0":
version "7.0.0"
@@ -163,11 +163,11 @@
integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==
"@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.4.tgz#a47e02bc91fb259d2e6727c2a30013e3ac13c4a2"
- integrity sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q==
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.5.5.tgz#0aa6824f7100a2e0e89c1527c23936c152cab351"
+ integrity sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==
dependencies:
- lodash "^4.17.11"
+ lodash "^4.17.13"
"@babel/helper-remap-async-to-generator@^7.1.0":
version "7.1.0"
@@ -225,9 +225,9 @@
"@babel/types" "^7.5.5"
"@babel/highlight@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
- integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==
+ version "7.5.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540"
+ integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==
dependencies:
chalk "^2.0.0"
esutils "^2.0.2"
@@ -252,7 +252,7 @@
"@babel/helper-remap-async-to-generator" "^7.1.0"
"@babel/plugin-syntax-async-generators" "^7.2.0"
-"@babel/plugin-proposal-class-properties@^7.1.0", "@babel/plugin-proposal-class-properties@^7.4.4":
+"@babel/plugin-proposal-class-properties@^7.1.0", "@babel/plugin-proposal-class-properties@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz#a974cfae1e37c3110e71f3c6a2e48b8e71958cd4"
integrity sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==
@@ -833,7 +833,7 @@
"@babel/helper-regex" "^7.4.4"
regexpu-core "^4.5.4"
-"@babel/preset-env@^7.1.0", "@babel/preset-env@^7.4.4":
+"@babel/preset-env@^7.1.0", "@babel/preset-env@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.5.tgz#bc470b53acaa48df4b8db24a570d6da1fef53c9a"
integrity sha512-GMZQka/+INwsMz1A5UEql8tG015h5j/qjptpKY2gJ7giy8ohzU710YciJB5rcKsWGWHiW3RUnHib0E5/m3Tp3A==
@@ -914,9 +914,9 @@
integrity sha512-FBMd0IiARPtH5aaOFUVki6evHiJQiY0pFy7fizyRF7dtwc+el3nwpzvhb9qBNzceG1OIJModG1xpE0DDFjPXwA==
"@babel/standalone@^7.0.0":
- version "7.3.4"
- resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.3.4.tgz#b622c1e522acef91b2a14f22bdcdd4f935a1a474"
- integrity sha512-4L9c5i4WlGqbrjOVX0Yp8TIR5cEiw1/tPYYZENW/iuO2uI6viY38U7zALidzNfGdZIwNc+A/AWqMEWKeScWkBg==
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.5.5.tgz#9d3143f6078ff408db694a4254bd6f03c5c33962"
+ integrity sha512-YIp5taErC4uvp4d5urJtWMui3cpvZt83x57l4oVJNvFtDzumf3pMgRmoTSpGuEzh1yzo7jHhg3mbQmMhmKPbjA==
"@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4":
version "7.4.4"