summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzesiek.bizon@gmail.com>2018-12-04 09:11:33 +0100
committerGrzegorz Bizon <grzesiek.bizon@gmail.com>2018-12-04 09:11:33 +0100
commit6b4926f2aa9df7582778e3e50b8841d673f1b84c (patch)
treef292d22e9757913bb91ba38bf64a05f4b16f3130
parent68780d29ad79627da94d805855254b43d354cfdf (diff)
parent83f0798e7dc588f0e4cb6816daadeef7dbfc8b81 (diff)
downloadgitlab-ce-6b4926f2aa9df7582778e3e50b8841d673f1b84c.tar.gz
Merge commit '83f0798e7dc588f0e4cb6816daadeef7dbfc8b81' into fix/gb/encrypt-runners-tokens
* commit '83f0798e7dc588f0e4cb6816daadeef7dbfc8b81': (101 commits)
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md2
-rw-r--r--CHANGELOG.md24
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock12
-rw-r--r--Gemfile.rails4.lock12
-rw-r--r--app/assets/javascripts/api.js1
-rw-r--r--app/assets/javascripts/diffs/components/app.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue17
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue9
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue20
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue29
-rw-r--r--app/assets/javascripts/diffs/store/actions.js8
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js3
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js7
-rw-r--r--app/assets/javascripts/groups_select.js7
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue2
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js38
-rw-r--r--app/assets/javascripts/mirrors/mirror_repos.js1
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue28
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js34
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue242
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue18
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue321
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue98
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js2
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/wikis/wikis.js4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue6
-rw-r--r--app/assets/javascripts/star.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue110
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue74
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue48
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue50
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue11
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/alerts.scss4
-rw-r--r--app/assets/stylesheets/framework/callout.scss6
-rw-r--r--app/assets/stylesheets/framework/filters.scss16
-rw-r--r--app/assets/stylesheets/framework/highlight.scss1
-rw-r--r--app/assets/stylesheets/framework/icons.scss12
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss4
-rw-r--r--app/assets/stylesheets/framework/timeline.scss10
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss38
-rw-r--r--app/assets/stylesheets/pages/notes.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss5
-rw-r--r--app/assets/stylesheets/pages/tree.scss12
-rw-r--r--app/controllers/admin/impersonations_controller.rb13
-rw-r--r--app/controllers/admin/requests_profiles_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb5
-rw-r--r--app/controllers/application_controller.rb25
-rw-r--r--app/controllers/chaos_controller.rb10
-rw-r--r--app/controllers/metrics_controller.rb2
-rw-r--r--app/controllers/profiles/keys_controller.rb4
-rw-r--r--app/controllers/projects/environments_controller.rb2
-rw-r--r--app/helpers/users_helper.rb4
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/clusters/applications/jupyter.rb6
-rw-r--r--app/models/concerns/shardable.rb18
-rw-r--r--app/models/environment_status.rb14
-rw-r--r--app/models/notification_setting.rb2
-rw-r--r--app/models/pool_repository.rb11
-rw-r--r--app/models/project.rb8
-rw-r--r--app/models/project_repository.rb13
-rw-r--r--app/models/remote_mirror.rb2
-rw-r--r--app/models/wiki_page.rb6
-rw-r--r--app/services/access_token_validation_service.rb6
-rw-r--r--app/services/ci/archive_trace_service.rb35
-rw-r--r--app/services/ci/create_pipeline_service.rb10
-rw-r--r--app/services/files/multi_service.rb11
-rw-r--r--app/services/projects/create_service.rb4
-rw-r--r--app/services/projects/hashed_storage/migrate_repository_service.rb1
-rw-r--r--app/views/admin/runners/_sort_dropdown.html.haml2
-rw-r--r--app/views/admin/users/_head.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml6
-rw-r--r--app/views/groups/edit.html.haml1
-rw-r--r--app/views/invites/show.html.haml17
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/project_templates/_built_in_templates.html.haml6
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tree/_tree_content.html.haml2
-rw-r--r--app/views/projects/wikis/_sidebar_wiki_page.html.haml2
-rw-r--r--app/views/projects/wikis/edit.html.haml8
-rw-r--r--app/views/projects/wikis/history.html.haml4
-rw-r--r--app/views/projects/wikis/show.html.haml6
-rw-r--r--app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml2
-rw-r--r--app/views/shared/_milestones_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/groups/_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/views/shared/labels/_sort_dropdown.html.haml2
-rw-r--r--app/workers/archive_trace_worker.rb2
-rw-r--r--app/workers/ci/archive_traces_cron_worker.rb14
-rw-r--r--app/workers/pipeline_schedule_worker.rb32
-rw-r--r--changelogs/unreleased/1979-redesign-mr-widget-approvals-ce.yml5
-rw-r--r--changelogs/unreleased/40385-prohibit_impersonation.yml5
-rw-r--r--changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml5
-rw-r--r--changelogs/unreleased/50264-add-border-around-the-repository-file-tree.yml5
-rw-r--r--changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml5
-rw-r--r--changelogs/unreleased/51083-fix-move-api.yml5
-rw-r--r--changelogs/unreleased/52370-filter-by-none-any-for-labels-in-issues-mrs-boards.yml5
-rw-r--r--changelogs/unreleased/53400-unstar-icon-button-is-misaligned.yml5
-rw-r--r--changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml5
-rw-r--r--changelogs/unreleased/53778-remove-site-statistics.yml5
-rw-r--r--changelogs/unreleased/54048-Line-numbers-are-misaligned-in-file-blame-view.yml5
-rw-r--r--changelogs/unreleased/54218-fix-mergeUrlParams.yml5
-rw-r--r--changelogs/unreleased/54282-tooltip-stuck.yml5
-rw-r--r--changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml5
-rw-r--r--changelogs/unreleased/ab-approximate-counts.yml5
-rw-r--r--changelogs/unreleased/bump_gpgme_gem.yml5
-rw-r--r--changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml5
-rw-r--r--changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml5
-rw-r--r--changelogs/unreleased/dm-batch-loader-sidekiq.yml5
-rw-r--r--changelogs/unreleased/document-raw-snippet-api.yml5
-rw-r--r--changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml5
-rw-r--r--changelogs/unreleased/fix-not-render-emoji.yml5
-rw-r--r--changelogs/unreleased/gt-externalize-app-views-invites.yml5
-rw-r--r--changelogs/unreleased/ignore-failed-pipeline-creation-on-pipeline-schedule.yml5
-rw-r--r--changelogs/unreleased/improve_auto_devops_migration_debug.yml6
-rw-r--r--changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml5
-rw-r--r--changelogs/unreleased/jupyter-tls.yml5
-rw-r--r--changelogs/unreleased/non-webkit-scrollbar-fixing.yml5
-rw-r--r--changelogs/unreleased/order-of-notification-settings.yml5
-rw-r--r--changelogs/unreleased/render-text-deprecated.yml6
-rw-r--r--changelogs/unreleased/sh-fix-hash-filename-handling.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-38317.yml5
-rw-r--r--changelogs/unreleased/sh-fix-mirrors-protected-branches.yml5
-rw-r--r--changelogs/unreleased/tc-repo-full-path-in-db.yml5
-rw-r--r--changelogs/unreleased/winh-merge-request-commit-discussion.yml5
-rw-r--r--changelogs/unreleased/workhorse-7-3-0.yml5
-rw-r--r--config/gitlab.yml.example3
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--db/migrate/20181122160027_create_project_repositories.rb18
-rw-r--r--db/post_migrate/20181123042307_drop_site_statistics.rb (renamed from db/migrate/20181123042307_drop_site_statistics.rb)0
-rw-r--r--db/schema.rb11
-rw-r--r--doc/README.md6
-rw-r--r--doc/administration/gitaly/index.md11
-rw-r--r--doc/administration/monitoring/performance/request_profiling.md17
-rw-r--r--doc/api/README.md36
-rw-r--r--doc/api/commits.md2
-rw-r--r--doc/api/repositories.md8
-rw-r--r--doc/api/snippets.md40
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md6
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/code_review.md19
-rw-r--r--doc/development/ee_features.md2
-rw-r--r--doc/development/fe_guide/graphql.md83
-rw-r--r--doc/development/fe_guide/index.md3
-rw-r--r--doc/topics/authentication/index.md1
-rw-r--r--doc/topics/autodevops/index.md13
-rw-r--r--doc/user/admin_area/custom_project_templates.md25
-rw-r--r--doc/user/group/custom_project_templates.md23
-rw-r--r--doc/user/group/index.md5
-rw-r--r--doc/user/markdown.md2
-rw-r--r--doc/user/project/clusters/index.md53
-rw-r--r--doc/user/project/pipelines/schedules.md12
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/api_guard.rb6
-rw-r--r--lib/api/award_emoji.rb2
-rw-r--r--lib/api/badges.rb2
-rw-r--r--lib/api/boards.rb2
-rw-r--r--lib/api/branches.rb4
-rw-r--r--lib/api/commit_statuses.rb2
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/deploy_keys.rb2
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/discussions.rb2
-rw-r--r--lib/api/environments.rb2
-rw-r--r--lib/api/events.rb2
-rw-r--r--lib/api/files.rb2
-rw-r--r--lib/api/group_boards.rb2
-rw-r--r--lib/api/group_milestones.rb2
-rw-r--r--lib/api/group_variables.rb2
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/issues.rb4
-rw-r--r--lib/api/job_artifacts.rb2
-rw-r--r--lib/api/jobs.rb2
-rw-r--r--lib/api/labels.rb2
-rw-r--r--lib/api/members.rb2
-rw-r--r--lib/api/merge_request_diffs.rb2
-rw-r--r--lib/api/merge_requests.rb4
-rw-r--r--lib/api/namespaces.rb2
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/notification_settings.rb2
-rw-r--r--lib/api/pages_domains.rb4
-rw-r--r--lib/api/pipeline_schedules.rb2
-rw-r--r--lib/api/pipelines.rb2
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/project_import.rb2
-rw-r--r--lib/api/project_milestones.rb2
-rw-r--r--lib/api/project_snippets.rb2
-rw-r--r--lib/api/projects.rb4
-rw-r--r--lib/api/protected_branches.rb4
-rw-r--r--lib/api/protected_tags.rb4
-rw-r--r--lib/api/repositories.rb2
-rw-r--r--lib/api/resource_label_events.rb2
-rw-r--r--lib/api/runners.rb2
-rw-r--r--lib/api/search.rb4
-rw-r--r--lib/api/services.rb4
-rw-r--r--lib/api/subscriptions.rb2
-rw-r--r--lib/api/tags.rb4
-rw-r--r--lib/api/todos.rb2
-rw-r--r--lib/api/triggers.rb2
-rw-r--r--lib/api/variables.rb2
-rw-r--r--lib/api/wikis.rb4
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb3
-rw-r--r--lib/gitlab/ci/config.rb4
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb8
-rw-r--r--lib/gitlab/ci/config/entry/attributable.rb29
-rw-r--r--lib/gitlab/ci/config/entry/boolean.rb20
-rw-r--r--lib/gitlab/ci/config/entry/cache.rb8
-rw-r--r--lib/gitlab/ci/config/entry/commands.rb4
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb83
-rw-r--r--lib/gitlab/ci/config/entry/coverage.rb4
-rw-r--r--lib/gitlab/ci/config/entry/environment.rb4
-rw-r--r--lib/gitlab/ci/config/entry/factory.rb75
-rw-r--r--lib/gitlab/ci/config/entry/global.rb6
-rw-r--r--lib/gitlab/ci/config/entry/hidden.rb4
-rw-r--r--lib/gitlab/ci/config/entry/image.rb4
-rw-r--r--lib/gitlab/ci/config/entry/job.rb6
-rw-r--r--lib/gitlab/ci/config/entry/jobs.rb6
-rw-r--r--lib/gitlab/ci/config/entry/key.rb4
-rw-r--r--lib/gitlab/ci/config/entry/legacy_validation_helpers.rb72
-rw-r--r--lib/gitlab/ci/config/entry/node.rb103
-rw-r--r--lib/gitlab/ci/config/entry/paths.rb4
-rw-r--r--lib/gitlab/ci/config/entry/policy.rb14
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb6
-rw-r--r--lib/gitlab/ci/config/entry/retry.rb14
-rw-r--r--lib/gitlab/ci/config/entry/script.rb4
-rw-r--r--lib/gitlab/ci/config/entry/service.rb2
-rw-r--r--lib/gitlab/ci/config/entry/services.rb6
-rw-r--r--lib/gitlab/ci/config/entry/simplifiable.rb45
-rw-r--r--lib/gitlab/ci/config/entry/stage.rb4
-rw-r--r--lib/gitlab/ci/config/entry/stages.rb4
-rw-r--r--lib/gitlab/ci/config/entry/undefined.rb42
-rw-r--r--lib/gitlab/ci/config/entry/unspecified.rb21
-rw-r--r--lib/gitlab/ci/config/entry/validatable.rb40
-rw-r--r--lib/gitlab/ci/config/entry/validator.rb28
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb198
-rw-r--r--lib/gitlab/ci/config/entry/variables.rb4
-rw-r--r--lib/gitlab/ci/config/external/file/base.rb4
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/yaml_processor.rb2
-rw-r--r--lib/gitlab/config/entry/attributable.rb27
-rw-r--r--lib/gitlab/config/entry/boolean.rb18
-rw-r--r--lib/gitlab/config/entry/configurable.rb81
-rw-r--r--lib/gitlab/config/entry/factory.rb73
-rw-r--r--lib/gitlab/config/entry/legacy_validation_helpers.rb70
-rw-r--r--lib/gitlab/config/entry/node.rb101
-rw-r--r--lib/gitlab/config/entry/simplifiable.rb43
-rw-r--r--lib/gitlab/config/entry/undefined.rb40
-rw-r--r--lib/gitlab/config/entry/unspecified.rb19
-rw-r--r--lib/gitlab/config/entry/validatable.rb38
-rw-r--r--lib/gitlab/config/entry/validator.rb26
-rw-r--r--lib/gitlab/config/entry/validators.rb196
-rw-r--r--lib/gitlab/config/loader/format_error.rb9
-rw-r--r--lib/gitlab/config/loader/yaml.rb (renamed from lib/gitlab/ci/config/loader.rb)12
-rw-r--r--lib/gitlab/database/count.rb79
-rw-r--r--lib/gitlab/database/count/exact_count_strategy.rb31
-rw-r--r--lib/gitlab/database/count/reltuples_count_strategy.rb79
-rw-r--r--lib/gitlab/database/count/tablesample_count_strategy.rb66
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb3
-rw-r--r--lib/tasks/gettext.rake4
-rw-r--r--locale/gitlab.pot42
-rw-r--r--package.json4
-rw-r--r--public/robots.txt2
-rwxr-xr-xscripts/trigger-build14
-rw-r--r--spec/controllers/admin/users_controller_spec.rb12
-rw-r--r--spec/controllers/application_controller_spec.rb6
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb9
-rw-r--r--spec/features/admin/admin_users_spec.rb101
-rw-r--r--spec/features/boards/modal_filter_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb13
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb4
-rw-r--r--spec/features/issues/user_sorts_issues_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_deployment_widget_spec.rb16
-rw-r--r--spec/features/merge_request/user_sees_discussions_spec.rb21
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb2
-rw-r--r--spec/features/merge_requests/user_sorts_merge_requests_spec.rb6
-rw-r--r--spec/features/projects/environments/environment_spec.rb10
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb4
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb56
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb16
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb6
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb8
-rw-r--r--spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json166
-rw-r--r--spec/fixtures/security-reports/master/gl-dependency-scanning-report.json161
-rw-r--r--spec/javascripts/diffs/components/app_spec.js13
-rw-r--r--spec/javascripts/diffs/components/diff_table_cell_spec.js37
-rw-r--r--spec/javascripts/diffs/components/inline_diff_table_row_spec.js42
-rw-r--r--spec/javascripts/diffs/components/parallel_diff_table_row_spec.js85
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js13
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js10
-rw-r--r--spec/javascripts/lib/utils/url_utility_spec.js24
-rw-r--r--spec/javascripts/monitoring/graph_spec.js20
-rw-r--r--spec/javascripts/monitoring/mock_data.js53
-rw-r--r--spec/javascripts/monitoring/monitoring_store_spec.js34
-rw-r--r--spec/javascripts/pipelines/graph/job_item_spec.js52
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js51
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js30
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js90
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js13
-rw-r--r--spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js40
-rw-r--r--spec/lib/gitlab/auth/user_auth_finders_spec.rb15
-rw-r--r--spec/lib/gitlab/config/entry/attributable_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/attributable_spec.rb)6
-rw-r--r--spec/lib/gitlab/config/entry/boolean_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/boolean_spec.rb)2
-rw-r--r--spec/lib/gitlab/config/entry/configurable_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/configurable_spec.rb)8
-rw-r--r--spec/lib/gitlab/config/entry/factory_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/factory_spec.rb)14
-rw-r--r--spec/lib/gitlab/config/entry/simplifiable_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb)2
-rw-r--r--spec/lib/gitlab/config/entry/undefined_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/undefined_spec.rb)2
-rw-r--r--spec/lib/gitlab/config/entry/unspecified_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/unspecified_spec.rb)2
-rw-r--r--spec/lib/gitlab/config/entry/validatable_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/validatable_spec.rb)8
-rw-r--r--spec/lib/gitlab/config/entry/validator_spec.rb (renamed from spec/lib/gitlab/ci/config/entry/validator_spec.rb)2
-rw-r--r--spec/lib/gitlab/config/loader/yaml_spec.rb (renamed from spec/lib/gitlab/ci/config/loader_spec.rb)6
-rw-r--r--spec/lib/gitlab/database/count/exact_count_strategy_spec.rb34
-rw-r--r--spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb48
-rw-r--r--spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb65
-rw-r--r--spec/lib/gitlab/database/count_spec.rb72
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/migrations/clean_up_for_members_spec.rb5
-rw-r--r--spec/migrations/delete_inconsistent_internal_id_records_spec.rb15
-rw-r--r--spec/models/environment_status_spec.rb39
-rw-r--r--spec/models/pool_repository_spec.rb2
-rw-r--r--spec/models/project_repository_spec.rb23
-rw-r--r--spec/models/project_spec.rb25
-rw-r--r--spec/models/remote_mirror_spec.rb8
-rw-r--r--spec/models/todo_spec.rb3
-rw-r--r--spec/requests/api/helpers_spec.rb13
-rw-r--r--spec/requests/api/namespaces_spec.rb2
-rw-r--r--spec/services/ci/archive_trace_service_spec.rb51
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb33
-rw-r--r--spec/services/files/multi_service_spec.rb39
-rw-r--r--spec/support/helpers/features/sorting_helpers.rb2
-rw-r--r--spec/support/helpers/sorting_helper.rb2
-rw-r--r--spec/workers/archive_trace_worker_spec.rb8
-rw-r--r--spec/workers/ci/archive_traces_cron_worker_spec.rb10
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb78
-rw-r--r--vendor/jupyter/values.yaml1
-rw-r--r--yarn.lock18
357 files changed, 4364 insertions, 2056 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7297d1f0eb6..79ec1b881d4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -962,6 +962,7 @@ review-deploy:
GITLAB_ADMIN_PASSWORD: "${REVIEW_APPS_ROOT_PASSWORD}"
GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
+ QA_DEBUG: "true"
artifacts:
paths:
- ./qa/gitlab-qa-run-*
@@ -977,6 +978,7 @@ review-deploy:
review-qa-smoke:
<<: *review-qa-base
+ retry: 2
script:
- gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 69cf7fe1548..08651195d98 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -16,6 +16,7 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
+- [ ] Add a link to this issue on the original security issue.
#### Backports
@@ -37,6 +38,7 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
+- [ ] Once your `master` MR is merged, comment on the original security issue with a link to that MR indicating the issue is fixed.
### Summary
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 57e946befb1..d41e5c8642f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.5.2 (2018-12-03)
+
+### Removed (1 change)
+
+- Removed Site Statistics optimization as it was causing problems. !23314
+
+### Fixed (6 changes, 1 of them is from the community)
+
+- Display impersonation token value only after creation. !22916
+- Fix not render emoji in filter dropdown. !23112 (Hiroyuki Sato)
+- Fixes stuck tooltip on stop env button. !23244
+- Correctly handle data-loss scenarios when encrypting columns. !23306
+- Clear BatchLoader context between Sidekiq jobs. !23308
+- Fix handling of filenames with hash characters in tree view. !23368
+
+
## 11.5.1 (2018-11-26)
### Security (17 changes)
@@ -287,6 +303,14 @@ entry.
- Disables stop environment button while the deploy is in progress.
+## 11.4.9 (2018-12-03)
+
+### Fixed (2 changes)
+
+- Display impersonation token value only after creation. !22916
+- Correctly handle data-loss scenarios when encrypting columns. !23306
+
+
## 11.4.8 (2018-11-27)
### Security (24 changes)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2dc8ac40dd4..4304f6c8744 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -181,4 +181,4 @@ This [documentation](doc/development/contributing/merge_request_workflow.md) has
## Style guides
-This [documentation](doc/development/contributing/design.md) has been moved.
+This [documentation](doc/development/contributing/style_guides.md) has been moved.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 9084fa2f716..26aaba0e866 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.1.0
+1.2.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index b26a34e4705..1502020768a 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-7.2.1
+7.3.0
diff --git a/Gemfile b/Gemfile
index c463c46a639..e37cadc128a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -82,7 +82,7 @@ gem 'validates_hostname', '~> 1.0.6'
gem 'browser', '~> 2.5'
# GPG
-gem 'gpgme'
+gem 'gpgme', '~> 2.0.18'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -91,7 +91,7 @@ gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap'
gem 'net-ldap'
# API
-gem 'grape', '~> 1.1'
+gem 'grape', '~> 1.1.0'
gem 'grape-entity', '~> 0.7.1'
gem 'rack-cors', '~> 1.0.0', require: 'rack/cors'
@@ -432,7 +432,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 1.1.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 1.2.0', require: 'gitaly'
gem 'grpc', '~> 1.15.0'
gem 'google-protobuf', '~> 3.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 96b453344a1..e7873932dad 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -273,7 +273,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.1.0)
+ gitaly-proto (1.2.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-default_value_for (3.1.1)
@@ -313,8 +313,8 @@ GEM
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
- gpgme (2.0.13)
- mini_portile2 (~> 2.1)
+ gpgme (2.0.18)
+ mini_portile2 (~> 2.3)
grape (1.1.0)
activesupport
builder
@@ -1006,7 +1006,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.1.0)
+ gitaly-proto (~> 1.2.0)
github-markup (~> 1.7.0)
gitlab-default_value_for (~> 3.1.1)
gitlab-markup (~> 1.6.5)
@@ -1016,8 +1016,8 @@ DEPENDENCIES
gon (~> 6.2)
google-api-client (~> 0.23)
google-protobuf (~> 3.6)
- gpgme
- grape (~> 1.1)
+ gpgme (~> 2.0.18)
+ grape (~> 1.1.0)
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.0)
grape_logging (~> 1.7)
diff --git a/Gemfile.rails4.lock b/Gemfile.rails4.lock
index 1289a28b719..7478e2173bd 100644
--- a/Gemfile.rails4.lock
+++ b/Gemfile.rails4.lock
@@ -272,7 +272,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (1.1.0)
+ gitaly-proto (1.2.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-markup (1.6.5)
@@ -310,8 +310,8 @@ GEM
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
- gpgme (2.0.13)
- mini_portile2 (~> 2.1)
+ gpgme (2.0.18)
+ mini_portile2 (~> 2.3)
grape (1.1.0)
activesupport
builder
@@ -998,7 +998,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 1.1.0)
+ gitaly-proto (~> 1.2.0)
github-markup (~> 1.7.0)
gitlab-markup (~> 1.6.5)
gitlab-sidekiq-fetcher
@@ -1007,8 +1007,8 @@ DEPENDENCIES
gon (~> 6.2)
google-api-client (~> 0.23)
google-protobuf (~> 3.6)
- gpgme
- grape (~> 1.1)
+ gpgme (~> 2.0.18)
+ grape (~> 1.1.0)
grape-entity (~> 0.7.1)
grape-path-helpers (~> 1.0)
grape_logging (~> 1.7)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 0da7ae1b229..f8dbe412f80 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,6 +5,7 @@ import axios from './lib/utils/axios_utils';
const Api = {
groupsPath: '/api/:version/groups.json',
groupPath: '/api/:version/groups/:id',
+ subgroupsPath: '/api/:version/groups/:id/subgroups',
namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 22da38ce7a5..bf9244df7f7 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -102,6 +102,12 @@ export default {
if (this.shouldShow) {
this.fetchData();
}
+
+ const id = window && window.location && window.location.hash;
+
+ if (id) {
+ this.setHighlightedRow(id.slice(1));
+ }
},
created() {
this.adjustView();
@@ -114,6 +120,7 @@ export default {
'fetchDiffFiles',
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
+ 'setHighlightedRow',
]),
fetchData() {
this.fetchDiffFiles()
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index aecdd133bf8..c0613d80d37 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -72,6 +72,13 @@ export default {
diffFiles: state => state.diffs.diffFiles,
}),
...mapGetters(['isLoggedIn']),
+ lineCode() {
+ return (
+ this.line.line_code ||
+ (this.line.left && this.line.line.left.line_code) ||
+ (this.line.right && this.line.right.line_code)
+ );
+ },
lineHref() {
return `#${this.line.line_code || ''}`;
},
@@ -97,7 +104,7 @@ export default {
},
},
methods: {
- ...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
+ ...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']),
handleCommentButton() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
},
@@ -168,7 +175,13 @@ export default {
>
<icon :size="12" name="comment" />
</button>
- <a v-if="lineNumber" :data-linenumber="lineNumber" :href="lineHref"> </a>
+ <a
+ v-if="lineNumber"
+ :data-linenumber="lineNumber"
+ :href="lineHref"
+ @click="setHighlightedRow(lineCode);"
+ >
+ </a>
<diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" />
</template>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index f4eb956adcb..d174b13e133 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapGetters, mapActions } from 'vuex';
import DiffLineGutterContent from './diff_line_gutter_content.vue';
import {
MATCH_LINE_TYPE,
@@ -30,6 +30,11 @@ export default {
type: String,
required: true,
},
+ isHighlighted: {
+ type: Boolean,
+ required: true,
+ default: false,
+ },
diffViewType: {
type: String,
required: false,
@@ -85,6 +90,7 @@ export default {
const { type } = this.line;
return {
+ hll: this.isHighlighted,
[type]: type,
[LINE_UNFOLD_CLASS_NAME]: this.isMatchLine,
[LINE_HOVER_CLASS_NAME]:
@@ -99,6 +105,7 @@ export default {
return this.lineType === OLD_LINE_TYPE ? this.line.old_line : this.line.new_line;
},
},
+ methods: mapActions('diffs', ['setHighlightedRow']),
};
</script>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 8d53fbded73..c764cbeb8e0 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters, mapActions } from 'vuex';
+import { mapGetters, mapActions, mapState } from 'vuex';
import DiffTableCell from './diff_table_cell.vue';
import {
NEW_LINE_TYPE,
@@ -40,6 +40,11 @@ export default {
};
},
computed: {
+ ...mapState({
+ isHighlighted(state) {
+ return this.line.line_code !== null && this.line.line_code === state.diffs.highlightedRow;
+ },
+ }),
...mapGetters('diffs', ['isInlineView']),
isContextLine() {
return this.line.type === CONTEXT_LINE_TYPE;
@@ -91,6 +96,7 @@ export default {
:is-bottom="isBottom"
:is-hover="isHover"
:show-comment-button="true"
+ :is-highlighted="isHighlighted"
class="diff-line-num old_line"
/>
<diff-table-cell
@@ -100,8 +106,18 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isHover"
+ :is-highlighted="isHighlighted"
class="diff-line-num new_line qa-new-diff-line"
/>
- <td :class="line.type" class="line_content" v-html="line.rich_text"></td>
+ <td
+ :class="[
+ line.type,
+ {
+ hll: isHighlighted,
+ },
+ ]"
+ class="line_content"
+ v-html="line.rich_text"
+ ></td>
</tr>
</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index 248dfd9815e..caf0df8a4e3 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import $ from 'jquery';
import DiffTableCell from './diff_table_cell.vue';
import {
@@ -43,6 +43,15 @@ export default {
};
},
computed: {
+ ...mapState({
+ isHighlighted(state) {
+ const lineCode =
+ (this.line.left && this.line.left.line_code) ||
+ (this.line.right && this.line.right.line_code);
+
+ return lineCode ? lineCode === state.diffs.highlightedRow : false;
+ },
+ }),
isContextLine() {
return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
},
@@ -57,7 +66,14 @@ export default {
return OLD_NO_NEW_LINE_TYPE;
}
- return this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
+ const lineTypeClass = this.line.left ? this.line.left.type : EMPTY_CELL_TYPE;
+
+ return [
+ lineTypeClass,
+ {
+ hll: this.isHighlighted,
+ },
+ ];
},
},
created() {
@@ -114,6 +130,7 @@ export default {
:line-type="oldLineType"
:is-bottom="isBottom"
:is-hover="isLeftHover"
+ :is-highlighted="isHighlighted"
:show-comment-button="true"
:diff-view-type="parallelDiffViewType"
line-position="left"
@@ -139,6 +156,7 @@ export default {
:line-type="newLineType"
:is-bottom="isBottom"
:is-hover="isRightHover"
+ :is-highlighted="isHighlighted"
:show-comment-button="true"
:diff-view-type="parallelDiffViewType"
line-position="right"
@@ -146,7 +164,12 @@ export default {
/>
<td
:id="line.right.line_code"
- :class="line.right.type"
+ :class="[
+ line.right.type,
+ {
+ hll: isHighlighted,
+ },
+ ]"
class="line_content parallel right-side"
@mousedown.native="handleParallelLineMouseDown"
v-html="line.right.rich_text"
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 0899d793c91..8b477c678fd 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -33,6 +33,10 @@ export const fetchDiffFiles = ({ state, commit }) => {
.then(handleLocationHash);
};
+export const setHighlightedRow = ({ commit }, lineCode) => {
+ commit(types.SET_HIGHLIGHTED_ROW, lineCode);
+};
+
// This is adding line discussions to the actual lines in the diff tree
// once for parallel and once for inline mode
export const assignDiscussionsToDiff = (
@@ -127,7 +131,7 @@ export const loadMoreLines = ({ commit }, options) => {
export const scrollToLineIfNeededInline = (_, line) => {
const hash = getLocationHash();
- if (hash && line.lineCode === hash) {
+ if (hash && line.line_code === hash) {
handleLocationHash();
}
};
@@ -137,7 +141,7 @@ export const scrollToLineIfNeededParallel = (_, line) => {
if (
hash &&
- ((line.left && line.left.lineCode === hash) || (line.right && line.right.lineCode === hash))
+ ((line.left && line.left.line_code === hash) || (line.right && line.right.line_code === hash))
) {
handleLocationHash();
}
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index 8fb2f0e17ac..98e57d52d77 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -26,4 +26,5 @@ export default () => ({
currentDiffFileId: '',
projectPath: '',
commentForms: [],
+ highlightedRow: null,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 74961a74899..0338cde3658 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -17,3 +17,4 @@ export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM';
+export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 7baf2ed17e6..f0895661bf2 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -241,4 +241,7 @@ export default {
[types.CLOSE_DIFF_FILE_COMMENT_FORM](state, fileHash) {
state.commentForms = state.commentForms.filter(form => form.fileHash !== fileHash);
},
+ [types.SET_HIGHLIGHTED_ROW](state, lineCode) {
+ state.highlightedRow = lineCode;
+ },
};
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index bb0ecb8efe7..b494b7e2de0 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -88,11 +88,16 @@ export const conditions = [
value: 'started',
},
{
- url: 'label_name[]=No+Label',
+ url: 'label_name[]=None',
tokenKey: 'label',
value: 'none',
},
{
+ url: 'label_name[]=Any',
+ tokenKey: 'any',
+ value: 'any',
+ },
+ {
url: 'my_reaction_emoji=None',
tokenKey: 'my-reaction',
value: 'none',
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index b4a3037c1b7..2049760fe29 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -10,13 +10,18 @@ export default function groupsSelect() {
const $select = $(this);
const allAvailable = $select.data('allAvailable');
const skipGroups = $select.data('skipGroups') || [];
+ const parentGroupID = $select.data('parentId');
+ const groupsPath = parentGroupID
+ ? Api.subgroupsPath.replace(':id', parentGroupID)
+ : Api.groupsPath;
+
$select.select2({
placeholder: 'Search for a group',
allowClear: $select.hasClass('allowClear'),
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
- url: Api.buildUrl(Api.groupsPath),
+ url: Api.buildUrl(groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index e318367a5ec..7a57ccf2dd3 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -105,7 +105,7 @@ export default {
:key="tabView.name"
class="h-100"
>
- <component :is="tabView.name" />
+ <component :is="tabView.component || tabView.name" />
</div>
</resizable-panel>
<nav class="ide-activity-bar">
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index a282c2df441..9850f7ce782 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -17,27 +17,29 @@ export function getParameterValues(sParam) {
// @param {Object} params - url keys and value to merge
// @param {String} url
export function mergeUrlParams(params, url) {
- let newUrl = Object.keys(params).reduce((acc, paramName) => {
- const paramValue = encodeURIComponent(params[paramName]);
- const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`);
-
- if (paramValue === null) {
- return acc.replace(pattern, '');
- } else if (url.search(pattern) !== -1) {
- return acc.replace(pattern, `$1${paramValue}$2`);
- }
-
- return `${acc}${acc.indexOf('?') > 0 ? '&' : '?'}${paramName}=${paramValue}`;
- }, decodeURIComponent(url));
+ const re = /^([^?#]*)(\?[^#]*)?(.*)/;
+ const merged = {};
+ const urlparts = url.match(re);
+
+ if (urlparts[2]) {
+ urlparts[2]
+ .substr(1)
+ .split('&')
+ .forEach(part => {
+ if (part.length) {
+ const kv = part.split('=');
+ merged[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('='));
+ }
+ });
+ }
- // Remove a trailing ampersand
- const lastChar = newUrl[newUrl.length - 1];
+ Object.assign(merged, params);
- if (lastChar === '&') {
- newUrl = newUrl.slice(0, -1);
- }
+ const query = Object.keys(merged)
+ .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(merged[key])}`)
+ .join('&');
- return newUrl;
+ return `${urlparts[1]}?${query}${urlparts[3]}`;
}
export function removeParamQueryString(url, param) {
diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js
index 0d8f31d6bfc..196b84621b6 100644
--- a/app/assets/javascripts/mirrors/mirror_repos.js
+++ b/app/assets/javascripts/mirrors/mirror_repos.js
@@ -30,6 +30,7 @@ export default class MirrorRepos {
this.$password.on('input.updateUrl', () => this.debouncedUpdateUrl());
this.initMirrorSSH();
+ this.updateProtectedBranches();
}
initMirrorSSH() {
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 815063237fc..64a1df80a8e 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -105,6 +105,9 @@ export default {
deploymentFlagData() {
return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag);
},
+ shouldRenderData() {
+ return this.graphData.queries.filter(s => s.result.length > 0).length > 0;
+ },
},
watch: {
hoverData() {
@@ -120,17 +123,17 @@ export default {
},
draw() {
const breakpointSize = bp.getBreakpointSize();
- const query = this.graphData.queries[0];
const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width;
+
this.margin = measurements.large.margin;
+
if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') {
this.graphHeight = 300;
this.margin = measurements.small.margin;
this.measurements = measurements.small;
}
- this.unitOfDisplay = query.unit || '';
+
this.yAxisLabel = this.graphData.y_label || 'Values';
- this.legendTitle = query.label || 'Average';
this.graphWidth = svgWidth - this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
this.baseGraphHeight = this.graphHeight - 50;
@@ -139,8 +142,15 @@ export default {
// pixel offsets inside the svg and outside are not 1:1
this.realPixelRatio = svgWidth / this.baseGraphWidth;
- this.renderAxesPaths();
- this.formatDeployments();
+ // set the legends on the axes
+ const [query] = this.graphData.queries;
+ this.legendTitle = query ? query.label : 'Average';
+ this.unitOfDisplay = query ? query.unit : '';
+
+ if (this.shouldRenderData) {
+ this.renderAxesPaths();
+ this.formatDeployments();
+ }
},
handleMouseOverGraph(e) {
let point = this.$refs.graphData.createSVGPoint();
@@ -266,7 +276,7 @@ export default {
:y-axis-label="yAxisLabel"
:unit-of-display="unitOfDisplay"
/>
- <svg ref="graphData" :viewBox="innerViewBox" class="graph-data">
+ <svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data">
<slot name="additionalSvgContent" :graphDrawData="graphDrawData" />
<graph-path
v-for="(path, index) in timeSeries"
@@ -293,8 +303,14 @@ export default {
@mousemove="handleMouseOverGraph($event);"
/>
</svg>
+ <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display">
+ <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">
+ {{ s__('Metrics|No data to display') }}
+ </text>
+ </svg>
</svg>
<graph-flag
+ v-if="shouldRenderData"
:real-pixel-ratio="realPixelRatio"
:current-x-coordinate="currentXCoordinate"
:current-data="currentData"
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 176f7d9eef2..8692c873a41 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -7,10 +7,29 @@ function sortMetrics(metrics) {
.value();
}
+function checkQueryEmptyData(query) {
+ return {
+ ...query,
+ result: query.result.filter(timeSeries => {
+ const newTimeSeries = timeSeries;
+ const hasValue = series =>
+ !Number.isNaN(series.value) && (series.value !== null || series.value !== undefined);
+ const hasNonNullValue = timeSeries.values.find(hasValue);
+
+ newTimeSeries.values = hasNonNullValue ? newTimeSeries.values : [];
+
+ return newTimeSeries.values.length > 0;
+ }),
+ };
+}
+
+function removeTimeSeriesNoData(queries) {
+ return queries.reduce((series, query) => series.concat(checkQueryEmptyData(query)), []);
+}
+
function normalizeMetrics(metrics) {
- return metrics.map(metric => ({
- ...metric,
- queries: metric.queries.map(query => ({
+ return metrics.map(metric => {
+ const queries = metric.queries.map(query => ({
...query,
result: query.result.map(result => ({
...result,
@@ -19,8 +38,13 @@ function normalizeMetrics(metrics) {
value: Number(value),
})),
})),
- })),
- }));
+ }));
+
+ return {
+ ...metric,
+ queries: removeTimeSeriesNoData(queries),
+ };
+ });
}
export default class MonitoringStore {
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 884ccca7bde..841fcec96e8 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -4,6 +4,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
import { __, sprintf } from '~/locale';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import Autosave from '../../autosave';
import {
@@ -30,6 +31,7 @@ export default {
markdownField,
userAvatarLink,
loadingButton,
+ TimelineEntryItem,
},
mixins: [issuableStateMixin],
props: {
@@ -309,137 +311,135 @@ Please check your network connection and try again.`;
<div>
<note-signed-out-widget v-if="!isLoggedIn" />
<discussion-locked-widget v-else-if="!canCreateNote" :issuable-type="issuableTypeTitle" />
- <div v-else-if="canCreateNote" class="notes notes-form timeline">
- <div class="timeline-entry note-form">
- <div class="timeline-entry-inner">
- <div class="flash-container error-alert timeline-content"></div>
- <div class="timeline-icon d-none d-sm-none d-md-block">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
- <div class="timeline-content timeline-content-form">
- <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
- <div class="error-alert"></div>
+ <ul v-else-if="canCreateNote" class="notes notes-form timeline">
+ <timeline-entry-item class="note-form">
+ <div class="flash-container error-alert timeline-content"></div>
+ <div class="timeline-icon d-none d-sm-none d-md-block">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ />
+ </div>
+ <div class="timeline-content timeline-content-form">
+ <form ref="commentForm" class="new-note common-note-form gfm-form js-main-target-form">
+ <div class="error-alert"></div>
- <issue-warning
- v-if="hasWarning(getNoteableData)"
- :is-locked="isLocked(getNoteableData)"
- :is-confidential="isConfidential(getNoteableData)"
- />
+ <issue-warning
+ v-if="hasWarning(getNoteableData)"
+ :is-locked="isLocked(getNoteableData)"
+ :is-confidential="isConfidential(getNoteableData)"
+ />
- <markdown-field
- ref="markdownField"
- :markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="markdownDocsPath"
- :quick-actions-docs-path="quickActionsDocsPath"
- :markdown-version="markdownVersion"
- :add-spacing-classes="false"
- >
- <textarea
- id="note-body"
- ref="textarea"
- slot="textarea"
- v-model="note"
- :disabled="isSubmitting"
- name="note[note]"
- class="note-textarea js-vue-comment-form js-note-text
+ <markdown-field
+ ref="markdownField"
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :markdown-version="markdownVersion"
+ :add-spacing-classes="false"
+ >
+ <textarea
+ id="note-body"
+ ref="textarea"
+ slot="textarea"
+ v-model="note"
+ :disabled="isSubmitting"
+ name="note[note]"
+ class="note-textarea js-vue-comment-form js-note-text
js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
- data-supports-quick-actions="true"
- aria-label="Description"
- placeholder="Write a comment or drag your files here…"
- @keydown.up="editCurrentUserLastNote();"
- @keydown.meta.enter="handleSave();"
- @keydown.ctrl.enter="handleSave();"
- >
- </textarea>
- </markdown-field>
- <div class="note-form-actions">
- <div
- class="float-left btn-group
+ data-supports-quick-actions="true"
+ aria-label="Description"
+ placeholder="Write a comment or drag your files here…"
+ @keydown.up="editCurrentUserLastNote();"
+ @keydown.meta.enter="handleSave();"
+ @keydown.ctrl.enter="handleSave();"
+ >
+ </textarea>
+ </markdown-field>
+ <div class="note-form-actions">
+ <div
+ class="float-left btn-group
append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
- >
- <button
- :disabled="isSubmitButtonDisabled"
- class="btn btn-create comment-btn js-comment-button js-comment-submit-button
+ >
+ <button
+ :disabled="isSubmitButtonDisabled"
+ class="btn btn-create comment-btn js-comment-button js-comment-submit-button
qa-comment-button"
- type="submit"
- @click.prevent="handleSave();"
- >
- {{ __(commentButtonTitle) }}
- </button>
- <button
- :disabled="isSubmitButtonDisabled"
- name="button"
- type="button"
- class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
- data-display="static"
- data-toggle="dropdown"
- aria-label="Open comment type dropdown"
- >
- <i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i>
- </button>
-
- <ul class="note-type-dropdown dropdown-open-top dropdown-menu">
- <li :class="{ 'droplab-item-selected': noteType === 'comment' }">
- <button
- type="button"
- class="btn btn-transparent"
- @click.prevent="setNoteType('comment');"
- >
- <i aria-hidden="true" class="fa fa-check icon"> </i>
- <div class="description">
- <strong>Comment</strong>
- <p>Add a general comment to this {{ noteableDisplayName }}.</p>
- </div>
- </button>
- </li>
- <li class="divider droplab-item-ignore"></li>
- <li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
- <button
- type="button"
- class="btn btn-transparent qa-discussion-option"
- @click.prevent="setNoteType('discussion');"
- >
- <i aria-hidden="true" class="fa fa-check icon"> </i>
- <div class="description">
- <strong>Start discussion</strong>
- <p>{{ startDiscussionDescription }}</p>
- </div>
- </button>
- </li>
- </ul>
- </div>
-
- <loading-button
- v-if="canUpdateIssue"
- :loading="isToggleStateButtonLoading"
- :container-class="[
- actionButtonClassNames,
- 'btn btn-comment btn-comment-and-close js-action-button',
- ]"
- :disabled="isToggleStateButtonLoading || isSubmitting"
- :label="issueActionButtonTitle"
- @click="handleSave(true);"
- />
-
+ type="submit"
+ @click.prevent="handleSave();"
+ >
+ {{ __(commentButtonTitle) }}
+ </button>
<button
- v-if="note.length"
+ :disabled="isSubmitButtonDisabled"
+ name="button"
type="button"
- class="btn btn-cancel js-note-discard"
- @click="discard"
+ class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
+ data-display="static"
+ data-toggle="dropdown"
+ aria-label="Open comment type dropdown"
>
- Discard draft
+ <i aria-hidden="true" class="fa fa-caret-down toggle-icon"> </i>
</button>
+
+ <ul class="note-type-dropdown dropdown-open-top dropdown-menu">
+ <li :class="{ 'droplab-item-selected': noteType === 'comment' }">
+ <button
+ type="button"
+ class="btn btn-transparent"
+ @click.prevent="setNoteType('comment');"
+ >
+ <i aria-hidden="true" class="fa fa-check icon"> </i>
+ <div class="description">
+ <strong>Comment</strong>
+ <p>Add a general comment to this {{ noteableDisplayName }}.</p>
+ </div>
+ </button>
+ </li>
+ <li class="divider droplab-item-ignore"></li>
+ <li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
+ <button
+ type="button"
+ class="btn btn-transparent qa-discussion-option"
+ @click.prevent="setNoteType('discussion');"
+ >
+ <i aria-hidden="true" class="fa fa-check icon"> </i>
+ <div class="description">
+ <strong>Start discussion</strong>
+ <p>{{ startDiscussionDescription }}</p>
+ </div>
+ </button>
+ </li>
+ </ul>
</div>
- </form>
- </div>
+
+ <loading-button
+ v-if="canUpdateIssue"
+ :loading="isToggleStateButtonLoading"
+ :container-class="[
+ actionButtonClassNames,
+ 'btn btn-comment btn-comment-and-close js-action-button',
+ ]"
+ :disabled="isToggleStateButtonLoading || isSubmitting"
+ :label="issueActionButtonTitle"
+ @click="handleSave(true);"
+ />
+
+ <button
+ v-if="note.length"
+ type="button"
+ class="btn btn-cancel js-note-discard"
+ @click="discard"
+ >
+ Discard draft
+ </button>
+ </div>
+ </form>
</div>
- </div>
- </div>
+ </timeline-entry-item>
+ </ul>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index ad58267b533..95164183ccb 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -48,13 +48,19 @@ export default {
required: false,
default: '',
},
+ resolveDiscussion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
updatedNoteBody: this.noteBody,
conflictWhileEditing: false,
isSubmitting: false,
- isResolving: false,
+ isResolving: this.resolveDiscussion,
+ isUnresolving: !this.resolveDiscussion,
resolveAsThread: true,
};
},
@@ -149,7 +155,7 @@ export default {
<div ref="editNoteForm" class="note-edit-form current-note-edit-form js-discussion-note-form">
<div v-if="conflictWhileEditing" class="js-conflict-edit-warning alert alert-danger">
This comment has changed since you started editing, please review the
- <a :href="noteHash" target="_blank" rel="noopener noreferrer"> updated comment </a> to ensure
+ <a :href="noteHash" target="_blank" rel="noopener noreferrer">updated comment</a> to ensure
information is not lost.
</div>
<div class="flash-container timeline-content"></div>
@@ -174,22 +180,20 @@ export default {
v-model="updatedNoteBody"
:data-supports-quick-actions="!isEditing"
name="note[note]"
- class="note-textarea js-gfm-input js-note-text
-js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
+ class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
aria-label="Description"
placeholder="Write a comment or drag your files here…"
@keydown.meta.enter="handleUpdate();"
@keydown.ctrl.enter="handleUpdate();"
@keydown.up="editMyLastNote();"
@keydown.esc="cancelHandler(true);"
- >
- </textarea>
+ ></textarea>
</markdown-field>
<div class="note-form-actions clearfix">
<button
:disabled="isDisabled"
type="button"
- class="js-vue-issue-save btn btn-success js-comment-button "
+ class="js-vue-issue-save btn btn-success js-comment-button"
@click="handleUpdate();"
>
{{ saveButtonTitle }}
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 36388e4b3cf..4eb3b49392c 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,10 +1,12 @@
<script>
+import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
-import { s__, __ } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
import systemNote from '~/vue_shared/components/notes/system_note.vue';
import icon from '~/vue_shared/components/icon.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -36,6 +38,7 @@ export default {
placeholderNote,
placeholderSystemNote,
systemNote,
+ TimelineEntryItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -156,6 +159,37 @@ export default {
(!discussion.diff_discussion && resolved && hasReplies && !isRepliesToggledByUser) || false
);
},
+ actionText() {
+ const commitId = this.discussion.commit_id ? truncateSha(this.discussion.commit_id) : '';
+ const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`;
+ const linkEnd = '</a>';
+
+ let text = s__('MergeRequests|started a discussion');
+
+ if (this.discussion.for_commit) {
+ text = s__(
+ 'MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}',
+ );
+ } else if (this.discussion.diff_discussion) {
+ if (this.discussion.active) {
+ text = s__('MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}');
+ } else {
+ text = s__(
+ 'MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}',
+ );
+ }
+ }
+
+ return sprintf(
+ text,
+ {
+ commitId,
+ linkStart,
+ linkEnd,
+ },
+ false,
+ );
+ },
},
watch: {
isReplying() {
@@ -269,179 +303,156 @@ Please check your network connection and try again.`;
</script>
<template>
- <li class="note note-discussion timeline-entry" :class="componentClassName">
- <div class="timeline-entry-inner">
- <div class="timeline-content">
- <div :data-discussion-id="discussion.id" class="discussion js-discussion-container">
- <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper">
- <div v-once class="timeline-icon">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
- <note-header
- :author="author"
- :created-at="initialDiscussion.created_at"
- :note-id="initialDiscussion.id"
- :include-toggle="true"
- :expanded="discussion.expanded"
- @toggleHandler="toggleDiscussionHandler"
- >
- <template v-if="discussion.diff_discussion">
- started a discussion on
- <a :href="discussion.discussion_path">
- <template v-if="discussion.active"
- >the diff</template
- >
- <template v-else
- >an old version of the diff</template
- >
- </a>
- </template>
- <template v-else-if="discussion.for_commit">
- started a discussion on commit
- <a :href="discussion.discussion_path">{{ truncateSha(discussion.commit_id) }}</a>
- </template>
- <template v-else
- >started a discussion</template
- >
- </note-header>
- <note-edited-text
- v-if="discussion.resolved"
- :edited-at="discussion.resolved_at"
- :edited-by="discussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline"
- />
- <note-edited-text
- v-else-if="lastUpdatedAt"
- :edited-at="lastUpdatedAt"
- :edited-by="lastUpdatedBy"
- action-text="Last updated"
- class-name="discussion-headline-light js-discussion-headline"
+ <timeline-entry-item class="note note-discussion" :class="componentClassName">
+ <div class="timeline-content">
+ <div :data-discussion-id="discussion.id" class="discussion js-discussion-container">
+ <div v-if="shouldRenderDiffs" class="discussion-header note-wrapper">
+ <div v-once class="timeline-icon">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
/>
</div>
- <div v-if="shouldShowDiscussions" class="discussion-body">
- <component
- :is="wrapperComponent"
- v-bind="wrapperComponentProps"
- class="card discussion-wrapper"
- >
- <div class="discussion-notes">
- <ul class="notes">
- <template v-if="shouldGroupReplies">
- <component
- :is="componentName(initialDiscussion)"
- :note="componentData(initialDiscussion)"
- @handleDeleteNote="deleteNoteHandler"
- >
- <slot slot="avatar-badge" name="avatar-badge"></slot>
- </component>
- <toggle-replies-widget
- v-if="hasReplies"
- :collapsed="isRepliesCollapsed"
- :replies="replies"
- @toggle="toggleReplies"
- />
- <template v-if="!isRepliesCollapsed">
- <component
- :is="componentName(note)"
- v-for="note in replies"
- :key="note.id"
- :note="componentData(note)"
- @handleDeleteNote="deleteNoteHandler"
- />
- </template>
- </template>
- <template v-else>
+ <note-header
+ :author="author"
+ :created-at="initialDiscussion.created_at"
+ :note-id="initialDiscussion.id"
+ :include-toggle="true"
+ :expanded="discussion.expanded"
+ @toggleHandler="toggleDiscussionHandler"
+ >
+ <span v-html="actionText"></span>
+ </note-header>
+ <note-edited-text
+ v-if="discussion.resolved"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ <note-edited-text
+ v-else-if="lastUpdatedAt"
+ :edited-at="lastUpdatedAt"
+ :edited-by="lastUpdatedBy"
+ action-text="Last updated"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ </div>
+ <div v-if="shouldShowDiscussions" class="discussion-body">
+ <component
+ :is="wrapperComponent"
+ v-bind="wrapperComponentProps"
+ class="card discussion-wrapper"
+ >
+ <div class="discussion-notes">
+ <ul class="notes">
+ <template v-if="shouldGroupReplies">
+ <component
+ :is="componentName(initialDiscussion)"
+ :note="componentData(initialDiscussion)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="isRepliesCollapsed"
+ :replies="replies"
+ @toggle="toggleReplies"
+ />
+ <template v-if="!isRepliesCollapsed">
<component
:is="componentName(note)"
- v-for="(note, index) in discussion.notes"
+ v-for="note in replies"
:key="note.id"
:note="componentData(note)"
@handleDeleteNote="deleteNoteHandler"
- >
- <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
- </component>
+ />
</template>
- </ul>
- <div
- v-if="!isRepliesCollapsed"
- :class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder"
- >
- <template v-if="!isReplying && canReply">
- <div class="discussion-with-resolve-btn">
+ </template>
+ <template v-else>
+ <component
+ :is="componentName(note)"
+ v-for="(note, index) in discussion.notes"
+ :key="note.id"
+ :note="componentData(note)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
+ </component>
+ </template>
+ </ul>
+ <div
+ v-if="!isRepliesCollapsed"
+ :class="{ 'is-replying': isReplying }"
+ class="discussion-reply-holder"
+ >
+ <template v-if="!isReplying && canReply">
+ <div class="discussion-with-resolve-btn">
+ <button
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply"
+ title="Add a reply"
+ @click="showReplyForm"
+ >
+ Reply...
+ </button>
+ <div v-if="discussion.resolvable">
<button
type="button"
- class="js-vue-discussion-reply btn btn-text-field mr-sm-2 qa-discussion-reply"
- title="Add a reply"
- @click="showReplyForm"
+ class="btn btn-default mr-sm-2"
+ @click="resolveHandler();"
>
- Reply...
+ <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i>
+ {{ resolveButtonTitle }}
</button>
- <div v-if="discussion.resolvable">
+ </div>
+ <div
+ v-if="discussion.resolvable"
+ class="btn-group discussion-actions ml-sm-2"
+ role="group"
+ >
+ <div v-if="!discussionResolved" class="btn-group" role="group">
+ <a
+ v-gl-tooltip
+ :href="discussion.resolve_with_issue_path"
+ :title="s__('MergeRequests|Resolve this discussion in a new issue')"
+ class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
+ >
+ <icon name="issue-new" />
+ </a>
+ </div>
+ <div v-if="hasUnresolvedDiscussions" class="btn-group" role="group">
<button
- type="button"
- class="btn btn-default mr-sm-2"
- @click="resolveHandler();"
+ v-gl-tooltip
+ class="btn btn-default discussion-next-btn"
+ title="Jump to next unresolved discussion"
+ @click="jumpToNextDiscussion"
>
- <i
- v-if="isResolving"
- aria-hidden="true"
- class="fa fa-spinner fa-spin"
- ></i>
- {{ resolveButtonTitle }}
+ <icon name="comment-next" />
</button>
</div>
- <div
- v-if="discussion.resolvable"
- class="btn-group discussion-actions ml-sm-2"
- role="group"
- >
- <div v-if="!discussionResolved" class="btn-group" role="group">
- <a
- v-gl-tooltip
- :href="discussion.resolve_with_issue_path"
- :title="s__('MergeRequests|Resolve this discussion in a new issue')"
- class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"
- >
- <icon name="issue-new" />
- </a>
- </div>
- <div v-if="hasUnresolvedDiscussions" class="btn-group" role="group">
- <button
- v-gl-tooltip
- class="btn btn-default discussion-next-btn"
- title="Jump to next unresolved discussion"
- @click="jumpToNextDiscussion"
- >
- <icon name="comment-next" />
- </button>
- </div>
- </div>
</div>
- </template>
- <note-form
- v-if="isReplying"
- ref="noteForm"
- :discussion="discussion"
- :is-editing="false"
- save-button-title="Comment"
- @handleFormUpdate="saveReply"
- @cancelForm="cancelReplyForm"
- />
- <note-signed-out-widget v-if="!canReply" />
- </div>
+ </div>
+ </template>
+ <note-form
+ v-if="isReplying"
+ ref="noteForm"
+ :discussion="discussion"
+ :is-editing="false"
+ save-button-title="Comment"
+ @handleFormUpdate="saveReply"
+ @cancelForm="cancelReplyForm"
+ />
+ <note-signed-out-widget v-if="!canReply" />
</div>
- </component>
- </div>
+ </div>
+ </component>
</div>
</div>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index fc37fe1c7af..a17be51353e 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -2,6 +2,7 @@
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
@@ -18,6 +19,7 @@ export default {
noteHeader,
noteActions,
noteBody,
+ TimelineEntryItem,
},
mixins: [noteable, resolvable],
props: {
@@ -169,62 +171,60 @@ export default {
</script>
<template>
- <li
+ <timeline-entry-item
:id="noteAnchorId"
:class="classNameBindings"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note timeline-entry note-wrapper"
+ class="note note-wrapper"
>
- <div class="timeline-entry-inner">
- <div v-once class="timeline-icon">
- <user-avatar-link
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- >
- <slot slot="avatar-badge" name="avatar-badge"> </slot>
- </user-avatar-link>
- </div>
- <div class="timeline-content">
- <div class="note-header">
- <note-header
- v-once
- :author="author"
- :created-at="note.created_at"
- :note-id="note.id"
- action-text="commented"
- />
- <note-actions
- :author-id="author.id"
- :note-id="note.id"
- :note-url="note.noteable_note_url"
- :access-level="note.human_access"
- :can-edit="note.current_user.can_edit"
- :can-award-emoji="note.current_user.can_award_emoji"
- :can-delete="note.current_user.can_edit"
- :can-report-as-abuse="canReportAsAbuse"
- :can-resolve="note.current_user.can_resolve"
- :report-abuse-path="note.report_abuse_path"
- :resolvable="note.resolvable"
- :is-resolved="note.resolved"
- :is-resolving="isResolving"
- :resolved-by="note.resolved_by"
- @handleEdit="editHandler"
- @handleDelete="deleteHandler"
- @handleResolve="resolveHandler"
- />
- </div>
- <note-body
- ref="noteBody"
- :note="note"
+ <div v-once class="timeline-icon">
+ <user-avatar-link
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ >
+ <slot slot="avatar-badge" name="avatar-badge"> </slot>
+ </user-avatar-link>
+ </div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <note-header
+ v-once
+ :author="author"
+ :created-at="note.created_at"
+ :note-id="note.id"
+ action-text="commented"
+ />
+ <note-actions
+ :author-id="author.id"
+ :note-id="note.id"
+ :note-url="note.noteable_note_url"
+ :access-level="note.human_access"
:can-edit="note.current_user.can_edit"
- :is-editing="isEditing"
- @handleFormUpdate="formUpdateHandler"
- @cancelForm="formCancelHandler"
+ :can-award-emoji="note.current_user.can_award_emoji"
+ :can-delete="note.current_user.can_edit"
+ :can-report-as-abuse="canReportAsAbuse"
+ :can-resolve="note.current_user.can_resolve"
+ :report-abuse-path="note.report_abuse_path"
+ :resolvable="note.resolvable"
+ :is-resolved="note.resolved"
+ :is-resolving="isResolving"
+ :resolved-by="note.resolved_by"
+ @handleEdit="editHandler"
+ @handleDelete="deleteHandler"
+ @handleResolve="resolveHandler"
/>
</div>
+ <note-body
+ ref="noteBody"
+ :note="note"
+ :can-edit="note.current_user.can_edit"
+ :is-editing="isEditing"
+ @handleFormUpdate="formUpdateHandler"
+ @cancelForm="formCancelHandler"
+ />
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index cd8394e0619..8edf3d088bb 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -36,7 +36,7 @@ export default {
const discussion = this.resolveAsThread;
const endpoint = discussion ? this.discussion.resolve_path : `${this.note.path}/resolve`;
- this.toggleResolveNote({ endpoint, isResolved, discussion })
+ return this.toggleResolveNote({ endpoint, isResolved, discussion })
.then(() => {
this.isResolving = false;
})
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 32b55575f95..01ef445c901 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -5,6 +5,7 @@ import initSettingsPanels from '~/settings_panels';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import { GROUP_BADGE } from '~/badges/constants';
+import groupsSelect from '~/groups_select';
import projectSelect from '~/project_select';
document.addEventListener('DOMContentLoaded', () => {
@@ -17,5 +18,8 @@ document.addEventListener('DOMContentLoaded', () => {
);
mountBadgeSettings(GROUP_BADGE);
+ // Initialize Subgroups selector
+ groupsSelect();
+
projectSelect();
});
diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index d3e8dbf4000..9b58d42b47d 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -1,5 +1,4 @@
import bp from '../../../breakpoints';
-import { slugify } from '../../../lib/utils/text_utility';
import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
@@ -26,7 +25,8 @@ export default class Wikis {
if (!this.newWikiForm) return;
const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
- const slug = slugify(slugInput.value);
+
+ const slug = slugInput.value;
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 6f008528db4..59cebaba717 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -18,23 +18,19 @@ export default {
required: true,
},
},
-
computed: {
graph() {
return this.pipeline.details && this.pipeline.details.stages;
},
},
-
methods: {
capitalizeStageName(name) {
const escapedName = _.escape(name);
return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
},
-
isFirstColumn(index) {
return index === 0;
},
-
stageConnectorClass(index, stage) {
let className;
@@ -48,7 +44,6 @@ export default {
return className;
},
-
refreshPipelineGraph() {
this.$emit('refreshPipelineGraph');
},
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 782494f72e4..cf9db89e32b 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -84,10 +84,6 @@ export default {
return textBuilder.join(' ');
},
-
- tooltipBoundary() {
- return this.dropdownLength < 5 ? 'viewport' : null;
- },
/**
* Verifies if the provided job has an action path
*
@@ -108,7 +104,7 @@ export default {
<div class="ci-job-component">
<gl-link
v-if="status.has_details"
- v-gl-tooltip="{ boundary: tooltipBoundary }"
+ v-gl-tooltip
:href="status.details_path"
:title="tooltipText"
:class="cssClassJobName"
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 007b83e1927..9af5d5b23cb 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -23,11 +23,11 @@ export default class Star {
if (isStarred) {
$starSpan.removeClass('starred').text(s__('StarProject|Star'));
$startIcon.remove();
- $this.prepend(spriteIcon('star-o'));
+ $this.prepend(spriteIcon('star-o', 'icon'));
} else {
$starSpan.addClass('starred').text(__('Unstar'));
$startIcon.remove();
- $this.prepend(spriteIcon('star'));
+ $this.prepend(spriteIcon('star', 'icon'));
}
})
.catch(() => Flash('Star toggle failed. Try again later.'));
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 950347d8863..2f2a37347af 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -112,7 +112,7 @@ export default {
</script>
<template>
- <div class="mr-widget-heading deploy-heading append-bottom-default">
+ <div class="deploy-heading">
<div class="ci-widget media">
<div class="media-body">
<div class="deploy-body">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue
new file mode 100644
index 00000000000..5967ca026e5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue
@@ -0,0 +1,6 @@
+<template>
+ <div class="mr-widget-heading">
+ <div class="mr-widget-content"><slot name="default"></slot></div>
+ <slot name="footer"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 6f422ea3f27..3b9fc2661ef 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
+import MrWidgetIcon from './mr_widget_icon.vue';
export default {
name: 'MRWidgetHeader',
@@ -13,6 +14,7 @@ export default {
Icon,
clipboardButton,
TooltipOnTruncate,
+ MrWidgetIcon,
},
directives: {
tooltip,
@@ -76,7 +78,7 @@ export default {
</script>
<template>
<div class="mr-source-target append-bottom-default">
- <div class="git-merge-icon-container append-right-default"><icon name="git-merge" /></div>
+ <mr-widget-icon name="git-merge" />
<div class="git-merge-container d-flex">
<div class="normal">
<strong>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
new file mode 100644
index 00000000000..e3adc7f7af5
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_icon.vue
@@ -0,0 +1,17 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ components: { Icon },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="circle-icon-container append-right-default"><icon :name="name" /></div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 71571ba9cab..f11cf21b0ca 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -79,67 +79,65 @@ export default {
</script>
<template>
- <div v-if="hasPipeline || hasCIError" class="mr-widget-heading append-bottom-default">
- <div class="ci-widget media">
- <template v-if="hasCIError">
- <div
- class="add-border ci-status-icon ci-status-icon-failed ci-error
- js-ci-error append-right-default"
- >
- <icon :size="32" name="status_failed_borderless" />
- </div>
- <div class="media-body" v-html="errorText"></div>
- </template>
- <template v-else-if="hasPipeline">
- <a :href="status.details_path" class="align-self-start append-right-default">
- <ci-icon :status="status" :size="32" :borderless="true" class="add-border" />
- </a>
- <div class="ci-widget-container d-flex">
- <div class="ci-widget-content">
- <div class="media-body">
- <div class="font-weight-bold">
- Pipeline
- <a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
- >#{{ pipeline.id }}</a
- >
+ <div v-if="hasPipeline || hasCIError" class="ci-widget media">
+ <template v-if="hasCIError">
+ <div
+ class="add-border ci-status-icon ci-status-icon-failed ci-error
+ js-ci-error append-right-default"
+ >
+ <icon :size="32" name="status_failed_borderless" />
+ </div>
+ <div class="media-body" v-html="errorText"></div>
+ </template>
+ <template v-else-if="hasPipeline">
+ <a :href="status.details_path" class="align-self-start append-right-default">
+ <ci-icon :status="status" :size="32" :borderless="true" class="add-border" />
+ </a>
+ <div class="ci-widget-container d-flex">
+ <div class="ci-widget-content">
+ <div class="media-body">
+ <div class="font-weight-bold">
+ Pipeline
+ <a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
+ >#{{ pipeline.id }}</a
+ >
- {{ pipeline.details.status.label }}
+ {{ pipeline.details.status.label }}
- <template v-if="hasCommitInfo">
- for
- <a
- :href="pipeline.commit.commit_path"
- class="commit-sha js-commit-link font-weight-normal"
- >
- {{ pipeline.commit.short_id }}</a
- >
- on
- <tooltip-on-truncate
- :title="sourceBranch"
- truncate-target="child"
- class="label-branch label-truncate"
- v-html="sourceBranchLink"
- />
- </template>
- </div>
- <div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div>
+ <template v-if="hasCommitInfo">
+ for
+ <a
+ :href="pipeline.commit.commit_path"
+ class="commit-sha js-commit-link font-weight-normal"
+ >
+ {{ pipeline.commit.short_id }}</a
+ >
+ on
+ <tooltip-on-truncate
+ :title="sourceBranch"
+ truncate-target="child"
+ class="label-branch label-truncate"
+ v-html="sourceBranchLink"
+ />
+ </template>
</div>
+ <div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div>
</div>
- <div>
- <span class="mr-widget-pipeline-graph">
- <span v-if="hasStages" class="stage-cell">
- <div
- v-for="(stage, i) in pipeline.details.stages"
- :key="i"
- class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
- >
- <pipeline-stage :stage="stage" />
- </div>
- </span>
+ </div>
+ <div>
+ <span class="mr-widget-pipeline-graph">
+ <span v-if="hasStages" class="stage-cell">
+ <div
+ v-for="(stage, i) in pipeline.details.stages"
+ :key="i"
+ class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
+ >
+ <pipeline-stage :stage="stage" />
+ </div>
</span>
- </div>
+ </span>
</div>
- </template>
- </div>
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
new file mode 100644
index 00000000000..5f5fe67b3c1
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -0,0 +1,74 @@
+<script>
+import Deployment from './deployment.vue';
+import MrWidgetContainer from './mr_widget_container.vue';
+import MrWidgetPipeline from './mr_widget_pipeline.vue';
+
+/**
+ * Renders the pipeline and related deployments from the store.
+ *
+ * | Props | Description
+ * |---------------|-------------
+ * | `mr` | This is the mr_widget store
+ * | `isPostMerge` | If true, show the "post merge" pipeline and deployments
+ */
+export default {
+ name: 'MrWidgetPipelineContainer',
+ components: {
+ Deployment,
+ MrWidgetContainer,
+ MrWidgetPipeline,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ isPostMerge: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ pipeline() {
+ return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
+ },
+ branch() {
+ return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranch;
+ },
+ branchLink() {
+ return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranchLink;
+ },
+ deployments() {
+ return this.isPostMerge ? this.mr.postMergeDeployments : this.mr.deployments;
+ },
+ deploymentClass() {
+ return this.isPostMerge ? 'js-post-deployment' : 'js-pre-deployment';
+ },
+ hasDeploymentMetrics() {
+ return this.isPostMerge;
+ },
+ },
+};
+</script>
+<template>
+ <mr-widget-container>
+ <mr-widget-pipeline
+ :pipeline="pipeline"
+ :ci-status="mr.ciStatus"
+ :has-ci="mr.hasCI"
+ :source-branch="branch"
+ :source-branch-link="branchLink"
+ :troubleshooting-docs-path="mr.troubleshootingDocsPath"
+ />
+ <div v-if="deployments.length" slot="footer" class="mr-widget-extension">
+ <deployment
+ v-for="deployment in deployments"
+ :key="deployment.id"
+ :class="deploymentClass"
+ :deployment="deployment"
+ :show-metrics="hasDeploymentMetrics"
+ />
+ </div>
+ </mr-widget-container>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index a269c0a4e87..3c3e3efcc36 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -6,7 +6,7 @@ import SmartInterval from '~/smart_interval';
import createFlash from '../flash';
import WidgetHeader from './components/mr_widget_header.vue';
import WidgetMergeHelp from './components/mr_widget_merge_help.vue';
-import WidgetPipeline from './components/mr_widget_pipeline.vue';
+import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue';
import Deployment from './components/deployment.vue';
import WidgetRelatedLinks from './components/mr_widget_related_links.vue';
import MergedState from './components/states/mr_widget_merged.vue';
@@ -44,7 +44,7 @@ export default {
components: {
'mr-widget-header': WidgetHeader,
'mr-widget-merge-help': WidgetMergeHelp,
- 'mr-widget-pipeline': WidgetPipeline,
+ MrWidgetPipelineContainer,
Deployment,
'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState,
@@ -296,23 +296,12 @@ export default {
<template>
<div class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" />
- <mr-widget-pipeline
+ <mr-widget-pipeline-container
v-if="shouldRenderPipelines"
- :pipeline="mr.pipeline"
- :ci-status="mr.ciStatus"
- :has-ci="mr.hasCI"
- :source-branch="mr.sourceBranch"
- :source-branch-link="mr.sourceBranchLink"
- :troubleshooting-docs-path="mr.troubleshootingDocsPath"
+ class="mr-widget-workflow"
+ :mr="mr"
/>
- <deployment
- v-for="deployment in mr.deployments"
- :key="`pre-merge-deploy-${deployment.id}`"
- class="js-pre-merge-deploy"
- :deployment="deployment"
- :show-metrics="false"
- />
- <div class="mr-section-container">
+ <div class="mr-section-container mr-widget-workflow">
<grouped-test-reports-app
v-if="mr.testResultsPath"
class="js-reports-container"
@@ -336,24 +325,11 @@ export default {
</div>
<div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div>
</div>
-
- <template v-if="shouldRenderMergedPipeline">
- <mr-widget-pipeline
- class="js-post-merge-pipeline prepend-top-default"
- :pipeline="mr.mergePipeline"
- :ci-status="mr.ciStatus"
- :has-ci="mr.hasCI"
- :source-branch="mr.targetBranch"
- :source-branch-link="mr.targetBranch"
- :troubleshooting-docs-path="mr.troubleshootingDocsPath"
- />
- <deployment
- v-for="postMergeDeployment in mr.postMergeDeployments"
- :key="`post-merge-deploy-${postMergeDeployment.id}`"
- :deployment="postMergeDeployment"
- :show-metrics="true"
- class="js-post-deployment"
- />
- </template>
+ <mr-widget-pipeline-container
+ v-if="shouldRenderMergedPipeline"
+ class="js-post-merge-pipeline mr-widget-workflow"
+ :mr="mr"
+ :is-post-merge="true"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index b1faebf409b..8d3a3009c55 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -17,12 +17,14 @@
* />
*/
import { mapGetters } from 'vuex';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default {
name: 'PlaceholderNote',
components: {
userAvatarLink,
+ TimelineEntryItem,
},
props: {
note: {
@@ -37,30 +39,28 @@ export default {
</script>
<template>
- <li class="note being-posted fade-in-half timeline-entry">
- <div class="timeline-entry-inner">
- <div class="timeline-icon">
- <user-avatar-link
- :link-href="getUserData.path"
- :img-src="getUserData.avatar_url"
- :img-size="40"
- />
- </div>
- <div :class="{ discussion: !note.individual_note }" class="timeline-content">
- <div class="note-header">
- <div class="note-header-info">
- <a :href="getUserData.path">
- <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span>
- <span class="note-headline-light">@{{ getUserData.username }}</span>
- </a>
- </div>
+ <timeline-entry-item class="note being-posted fade-in-half">
+ <div class="timeline-icon">
+ <user-avatar-link
+ :link-href="getUserData.path"
+ :img-src="getUserData.avatar_url"
+ :img-size="40"
+ />
+ </div>
+ <div :class="{ discussion: !note.individual_note }" class="timeline-content">
+ <div class="note-header">
+ <div class="note-header-info">
+ <a :href="getUserData.path">
+ <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span>
+ <span class="note-headline-light">@{{ getUserData.username }}</span>
+ </a>
</div>
- <div class="note-body">
- <div class="note-text">
- <p>{{ note.body }}</p>
- </div>
+ </div>
+ <div class="note-body">
+ <div class="note-text">
+ <p>{{ note.body }}</p>
</div>
</div>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue
index 674f923478d..7689425eb52 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue
@@ -1,4 +1,6 @@
<script>
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
/**
* Common component to render a placeholder system note.
*
@@ -9,6 +11,9 @@
*/
export default {
name: 'PlaceholderSystemNote',
+ components: {
+ TimelineEntryItem,
+ },
props: {
note: {
type: Object,
@@ -19,11 +24,9 @@ export default {
</script>
<template>
- <li class="note system-note timeline-entry being-posted fade-in-half">
- <div class="timeline-entry-inner">
- <div class="timeline-content">
- <em>{{ note.body }}</em>
- </div>
+ <timeline-entry-item class="note system-note being-posted fade-in-half">
+ <div class="timeline-content">
+ <em>{{ note.body }}</em>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index c6cf4661222..e61d1fd2031 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -1,22 +1,22 @@
<script>
import { GlSkeletonLoading } from '@gitlab/ui';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
export default {
name: 'SkeletonNote',
components: {
GlSkeletonLoading,
+ TimelineEntryItem,
},
};
</script>
<template>
- <li class="timeline-entry note note-wrapper">
- <div class="timeline-entry-inner">
- <div class="timeline-icon"></div>
- <div class="timeline-content">
- <div class="note-header"></div>
- <div class="note-body"><gl-skeleton-loading /></div>
- </div>
+ <timeline-entry-item class="note note-wrapper">
+ <div class="timeline-icon"></div>
+ <div class="timeline-content">
+ <div class="note-header"></div>
+ <div class="note-body"><gl-skeleton-loading /></div>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index fb86262d0b4..31df26f7b05 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -20,6 +20,7 @@ import $ from 'jquery';
import { mapGetters } from 'vuex';
import noteHeader from '~/notes/components/note_header.vue';
import Icon from '~/vue_shared/components/icon.vue';
+import TimelineEntryItem from './timeline_entry_item.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
@@ -29,6 +30,7 @@ export default {
components: {
Icon,
noteHeader,
+ TimelineEntryItem,
},
props: {
note: {
@@ -73,36 +75,34 @@ export default {
</script>
<template>
- <li
+ <timeline-entry-item
:id="noteAnchorId"
:class="{ target: isTargetNote }"
- class="note system-note timeline-entry note-wrapper"
+ class="note system-note note-wrapper"
>
- <div class="timeline-entry-inner">
- <div class="timeline-icon" v-html="iconHtml"></div>
- <div class="timeline-content">
- <div class="note-header">
- <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
- <span v-html="actionTextHtml"></span>
- </note-header>
- </div>
- <div class="note-body">
- <div
- :class="{
- 'system-note-commit-list': hasMoreCommits,
- 'hide-shade': expanded,
- }"
- class="note-text"
- v-html="note.note_html"
- ></div>
- <div v-if="hasMoreCommits" class="flex-list">
- <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;">
- <icon :name="toggleIcon" :size="8" class="append-right-5" />
- <span>Toggle commit list</span>
- </div>
+ <div class="timeline-icon" v-html="iconHtml"></div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
+ <span v-html="actionTextHtml"></span>
+ </note-header>
+ </div>
+ <div class="note-body">
+ <div
+ :class="{
+ 'system-note-commit-list': hasMoreCommits,
+ 'hide-shade': expanded,
+ }"
+ class="note-text"
+ v-html="note.note_html"
+ ></div>
+ <div v-if="hasMoreCommits" class="flex-list">
+ <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;">
+ <icon :name="toggleIcon" :size="8" class="append-right-5" />
+ <span>Toggle commit list</span>
</div>
</div>
</div>
</div>
- </li>
+ </timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue b/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue
new file mode 100644
index 00000000000..06974a12aed
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/notes/timeline_entry_item.vue
@@ -0,0 +1,11 @@
+<script>
+export default {
+ name: 'TimelineEntryItem',
+};
+</script>
+
+<template>
+ <li class="timeline-entry">
+ <div class="timeline-entry-inner"><slot></slot></div>
+ </li>
+</template>
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 4041f2b4479..d320fa5f595 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -6,6 +6,7 @@
@import 'bootstrap_migration';
@import 'framework/layout';
+@import 'framework/alerts';
@import 'framework/animations';
@import 'framework/vue_transitions';
@import 'framework/avatar';
diff --git a/app/assets/stylesheets/framework/alerts.scss b/app/assets/stylesheets/framework/alerts.scss
new file mode 100644
index 00000000000..866792a6a1b
--- /dev/null
+++ b/app/assets/stylesheets/framework/alerts.scss
@@ -0,0 +1,4 @@
+.alert-tip {
+ background-color: $theme-gray-100;
+ color: $theme-gray-900;
+}
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index bdd7f09d926..0d8e4afa76f 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -33,7 +33,11 @@
.bs-callout-warning {
background-color: $orange-100;
border-color: $orange-200;
- color: $orange-700;
+ color: $orange-900;
+
+ a {
+ color: $orange-900;
+ }
}
.bs-callout-info {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index d5693a5d1a1..f48b3ddc912 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -363,6 +363,12 @@
background-color: $white-light;
border-top: 0;
}
+
+ .filter-dropdown-container {
+ .dropdown {
+ margin-left: 0;
+ }
+ }
}
@include media-breakpoint-down(sm) {
@@ -372,16 +378,6 @@
.dropdown-menu {
width: 100%;
}
-
- .dropdown {
- margin-left: 0;
- }
-
- .fa-chevron-down {
- position: absolute;
- right: 10px;
- top: 10px;
- }
}
}
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 452e946f95f..a0bf6907b5f 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -42,6 +42,7 @@
padding: 10px;
text-align: right;
float: left;
+ line-height: 1;
a {
font-family: $monospace-font;
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index abd26e38d18..8db7d63266e 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -80,3 +80,15 @@
.user-avatar-link {
text-decoration: none;
}
+
+.circle-icon-container {
+ $border-size: 1px;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: $border-size solid $theme-gray-400;
+ border-radius: 50%;
+ padding: $gl-padding-8 - $border-size;
+ color: $theme-gray-700;
+}
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index de9e7c37695..19640ab5986 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -158,6 +158,10 @@
width: 100%;
}
+ .dropdown-menu-toggle {
+ margin-bottom: 0;
+ }
+
form {
display: block;
height: auto;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 4a311da1675..3d5208c3db5 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -31,16 +31,6 @@
.timeline-entry-inner {
position: relative;
-
- @include notes-media('max', map-get($grid-breakpoints, sm)) {
- .timeline-icon {
- display: none;
- }
-
- .timeline-content {
- margin-left: 0;
- }
- }
}
&:target,
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index b43bb3feef5..bf2868710eb 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -243,6 +243,7 @@ $gl-padding-top: 10px;
$gl-sidebar-padding: 22px;
$gl-bar-padding: 3px;
$input-horizontal-padding: 12px;
+$browserScrollbarSize: 10px;
/*
* Misc
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index b075009b57c..221b4e934ff 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -50,9 +50,19 @@
.mr-widget-heading {
position: relative;
border: 1px solid $border-color;
- border-radius: 4px;
+ border-radius: $border-radius-default;
+}
- &:not(.deploy-heading)::before {
+.mr-widget-extension {
+ border-top: 1px solid $border-color;
+ background-color: $gray-light;
+}
+
+.mr-widget-workflow {
+ margin-top: $gl-padding;
+ position: relative;
+
+ &::before {
content: '';
border-left: 1px solid $theme-gray-200;
position: absolute;
@@ -68,8 +78,8 @@
border-top: 0;
}
-.mr-widget-heading,
.mr-widget-section,
+.mr-widget-content,
.mr-widget-footer {
padding: $gl-padding;
}
@@ -560,19 +570,6 @@
color: $gl-text-color;
}
- .git-merge-icon-container {
- border: 1px solid $theme-gray-400;
- border-radius: 50%;
- height: 32px;
- width: 32px;
- color: $theme-gray-700;
- line-height: 28px;
-
- .ic-git-merge {
- vertical-align: middle;
- width: 31px;
- }
- }
.git-merge-container {
justify-content: space-between;
@@ -854,11 +851,6 @@
}
.deploy-heading {
- margin-top: -19px;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- background-color: $gray-light;
-
@include media-breakpoint-up(md) {
padding: $gl-padding-8 $gl-padding;
}
@@ -868,6 +860,10 @@
font-size: 12px;
margin-left: 48px;
}
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $border-color;
+ }
}
.deploy-body {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 4fda2964fd5..0d1d0b4d2d6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -589,12 +589,6 @@ $note-form-margin-left: 72px;
padding-bottom: 0;
}
-.note-header-author-name {
- @include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
- display: none;
- }
-}
-
.note-headline-light {
display: inline;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 80ec390d18e..6cc21072acd 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -723,7 +723,8 @@
.scrolling-tabs-container {
.scrolling-tabs {
margin-top: $gl-padding-8;
- margin-bottom: $gl-padding-8;
+ margin-bottom: $gl-padding-8 - $browserScrollbarSize;
+ padding-bottom: $browserScrollbarSize;
flex-wrap: wrap;
border-bottom: 0;
}
@@ -731,7 +732,7 @@
.fade-left,
.fade-right {
top: 0;
- height: 100%;
+ height: calc(100% - #{$browserScrollbarSize});
.fa {
top: 50%;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index dc5ca78ff58..a46b8679a42 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -104,11 +104,23 @@
border-bottom: 1px solid $white-normal;
border-top: 1px solid $white-normal;
+ &:last-of-type {
+ border-bottom-color: $white-light;
+ }
+
td,
th {
line-height: 21px;
}
+ th {
+ border-top-color: $gray-light;
+ }
+
+ td {
+ border-color: $border-color;
+ }
+
&:hover:not(.tree-truncated-warning) {
td {
background-color: $blue-50;
diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb
index 08d7e3b4fa2..65fe22bd8f4 100644
--- a/app/controllers/admin/impersonations_controller.rb
+++ b/app/controllers/admin/impersonations_controller.rb
@@ -5,23 +5,12 @@ class Admin::ImpersonationsController < Admin::ApplicationController
before_action :authenticate_impersonator!
def destroy
- original_user = current_user
-
- warden.set_user(impersonator, scope: :user)
-
- Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{original_user.username}")
-
- session[:impersonator_id] = nil
-
+ original_user = stop_impersonation
redirect_to admin_user_path(original_user), status: :found
end
private
- def impersonator
- @impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id]
- end
-
def authenticate_impersonator!
render_404 unless impersonator && impersonator.admin? && !impersonator.blocked?
end
diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb
index 64d74ae4231..57f7d3e3951 100644
--- a/app/controllers/admin/requests_profiles_controller.rb
+++ b/app/controllers/admin/requests_profiles_controller.rb
@@ -11,7 +11,7 @@ class Admin::RequestsProfilesController < Admin::ApplicationController
profile = Gitlab::RequestProfiler::Profile.find(clean_name)
if profile
- render text: profile.content
+ render html: profile.content
else
redirect_to admin_requests_profiles_path, alert: 'Profile not found'
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index b783c0e2a6f..e93be1c1ba2 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -2,6 +2,7 @@
class Admin::UsersController < Admin::ApplicationController
before_action :user, except: [:index, :new, :create]
+ before_action :check_impersonation_availability, only: :impersonate
def index
@users = User.order_name_asc.filter(params[:filter])
@@ -227,4 +228,8 @@ class Admin::UsersController < Admin::ApplicationController
result[:status] == :success
end
+
+ def check_impersonation_availability
+ access_denied! unless Gitlab.config.gitlab.impersonation_enabled
+ end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index dbb22127e82..65c1576d9d2 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -28,6 +28,7 @@ class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
before_action :set_usage_stats_consent_flag
+ before_action :check_impersonation_availability
around_action :set_locale
@@ -462,4 +463,28 @@ class ApplicationController < ActionController::Base
.new(settings, current_user, application_setting_params)
.execute
end
+
+ def check_impersonation_availability
+ return unless session[:impersonator_id]
+
+ unless Gitlab.config.gitlab.impersonation_enabled
+ stop_impersonation
+ access_denied! _('Impersonation has been disabled')
+ end
+ end
+
+ def stop_impersonation
+ impersonated_user = current_user
+
+ Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{impersonated_user.username}")
+
+ warden.set_user(impersonator, scope: :user)
+ session[:impersonator_id] = nil
+
+ impersonated_user
+ end
+
+ def impersonator
+ @impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id]
+ end
end
diff --git a/app/controllers/chaos_controller.rb b/app/controllers/chaos_controller.rb
index b4f46cddbe9..8d518c14b90 100644
--- a/app/controllers/chaos_controller.rb
+++ b/app/controllers/chaos_controller.rb
@@ -15,7 +15,7 @@ class ChaosController < ActionController::Base
duration_taken = (Time.now - start).seconds
Kernel.sleep duration_s - duration_taken if duration_s > duration_taken
- render text: "OK", content_type: 'text/plain'
+ render plain: "OK"
end
def cpuspin
@@ -24,14 +24,14 @@ class ChaosController < ActionController::Base
rand while Time.now < end_time
- render text: "OK", content_type: 'text/plain'
+ render plain: "OK"
end
def sleep
duration_s = (params[:duration_s]&.to_i || 30).seconds
Kernel.sleep duration_s
- render text: "OK", content_type: 'text/plain'
+ render plain: "OK"
end
def kill
@@ -44,13 +44,13 @@ class ChaosController < ActionController::Base
secret = ENV['GITLAB_CHAOS_SECRET']
# GITLAB_CHAOS_SECRET is required unless you're running in Development mode
if !secret && !Rails.env.development?
- render text: "chaos misconfigured: please configure GITLAB_CHAOS_SECRET when using GITLAB_ENABLE_CHAOS_ENDPOINTS outside of a development environment", content_type: 'text/plain', status: 500
+ render plain: "chaos misconfigured: please configure GITLAB_CHAOS_SECRET when using GITLAB_ENABLE_CHAOS_ENDPOINTS outside of a development environment", status: :internal_server_error
end
return unless secret
unless request.headers["HTTP_X_CHAOS_SECRET"] == secret
- render text: "To experience chaos, please set X-Chaos-Secret header", content_type: 'text/plain', status: 401
+ render plain: "To experience chaos, please set X-Chaos-Secret header", status: :unauthorized
end
end
end
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index 7353be478e1..c2089a0fca3 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -15,7 +15,7 @@ class MetricsController < ActionController::Base
"# Metrics are disabled, see: #{help_page}\n"
end
- render text: response, content_type: 'text/plain; version=0.0.4'
+ render plain: response, content_type: 'text/plain; version=0.0.4'
end
private
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 912421e3d08..dcee8eb7e6e 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -41,12 +41,12 @@ class Profiles::KeysController < Profiles::ApplicationController
user = UserFinder.new(params[:username]).find_by_username
if user.present?
headers['Content-Disposition'] = 'attachment'
- render text: user.all_ssh_keys.join("\n"), content_type: 'text/plain'
+ render plain: user.all_ssh_keys.join("\n")
else
return render_404
end
rescue => e
- render text: e.message
+ render html: e.message
end
else
return render_404
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index de10783df1a..e940f382a19 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -122,7 +122,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(terminal)
else
- render text: 'Not found', status: :not_found
+ render html: 'Not found', status: :not_found
end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 42b533ad772..bde9ca0cbf2 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -70,6 +70,10 @@ module UsersHelper
end
end
+ def impersonation_enabled?
+ Gitlab.config.gitlab.impersonation_enabled
+ end
+
private
def get_profile_tabs
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 9512ba42f67..1487b9d3bca 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -26,6 +26,8 @@ module Ci
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
+ has_many :deployments, through: :builds
+ has_many :environments, -> { distinct }, through: :deployments
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
@@ -523,10 +525,6 @@ module Ci
yaml_errors.present?
end
- def environments
- builds.where.not(environment: nil).success.pluck(:environment).uniq
- end
-
# Manually set the notes for a Ci::Pipeline
# There is no ActiveRecord relation between Ci::Pipeline and notes
# as they are related to a commit sha. This method helps importing
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index e43a0fd1786..421a923d386 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -56,7 +56,11 @@ module Clusters
def specification
{
"ingress" => {
- "hosts" => [hostname]
+ "hosts" => [hostname],
+ "tls" => [{
+ "hosts" => [hostname],
+ "secretName" => "jupyter-cert"
+ }]
},
"hub" => {
"extraEnv" => {
diff --git a/app/models/concerns/shardable.rb b/app/models/concerns/shardable.rb
new file mode 100644
index 00000000000..57cd77b44b4
--- /dev/null
+++ b/app/models/concerns/shardable.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Shardable
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :shard
+ validates :shard, presence: true
+ end
+
+ def shard_name
+ shard&.name
+ end
+
+ def shard_name=(name)
+ self.shard = Shard.by_name(name)
+ end
+end
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 4a128dde5cd..2fb6cadc8cd 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -12,13 +12,13 @@ class EnvironmentStatus
delegate :deployed_at, to: :deployment, allow_nil: true
def self.for_merge_request(mr, user)
- build_environments_status(mr, user, mr.diff_head_sha)
+ build_environments_status(mr, user, mr.actual_head_pipeline)
end
def self.after_merge_request(mr, user)
return [] unless mr.merged?
- build_environments_status(mr, user, mr.merge_commit_sha)
+ build_environments_status(mr, user, mr.merge_pipeline)
end
def initialize(environment, merge_request, sha)
@@ -61,13 +61,13 @@ class EnvironmentStatus
}
end
- def self.build_environments_status(mr, user, sha)
- Environment.where(project_id: [mr.source_project_id, mr.target_project_id])
- .available
- .with_deployment(sha).map do |environment|
+ def self.build_environments_status(mr, user, pipeline)
+ return [] unless pipeline
+
+ pipeline.environments.available.map do |environment|
next unless Ability.allowed?(user, :read_environment, environment)
- EnvironmentStatus.new(environment, mr, sha)
+ EnvironmentStatus.new(environment, mr, pipeline.sha)
end.compact
end
private_class_method :build_environments_status
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 1600acfc575..e82eaf4e069 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -5,7 +5,7 @@ class NotificationSetting < ActiveRecord::Base
ignore_column :events
- enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0, custom: 5 }
+ enum level: { global: 3, watch: 2, participating: 1, mention: 4, disabled: 0, custom: 5 }
default_value_for :level, NotificationSetting.levels[:global]
diff --git a/app/models/pool_repository.rb b/app/models/pool_repository.rb
index 7351674201e..bad0e30ceb5 100644
--- a/app/models/pool_repository.rb
+++ b/app/models/pool_repository.rb
@@ -1,21 +1,12 @@
# frozen_string_literal: true
class PoolRepository < ActiveRecord::Base
- belongs_to :shard
- validates :shard, presence: true
+ include Shardable
has_many :member_projects, class_name: 'Project'
after_create :correct_disk_path
- def shard_name
- shard&.name
- end
-
- def shard_name=(name)
- self.shard = Shard.by_name(name)
- end
-
private
def correct_disk_path
diff --git a/app/models/project.rb b/app/models/project.rb
index 9563e145bb8..1c5c34111b0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -186,6 +186,7 @@ class Project < ActiveRecord::Base
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_one :project_repository, inverse_of: :project
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
@@ -1206,6 +1207,13 @@ class Project < ActiveRecord::Base
false
end
+ def track_project_repository
+ return unless hashed_storage?(:repository)
+
+ project_repo = project_repository || build_project_repository
+ project_repo.update!(shard_name: repository_storage, disk_path: disk_path)
+ end
+
def create_repository(force: false)
# Forked import is handled asynchronously
return if forked? && !force
diff --git a/app/models/project_repository.rb b/app/models/project_repository.rb
new file mode 100644
index 00000000000..38913f3f2f5
--- /dev/null
+++ b/app/models/project_repository.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ProjectRepository < ActiveRecord::Base
+ include Shardable
+
+ belongs_to :project, inverse_of: :project_repository
+
+ class << self
+ def find_project(disk_path)
+ find_by(disk_path: disk_path)&.project
+ end
+ end
+end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index a3415a4a14c..b7b4d0f1be9 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -15,8 +15,6 @@ class RemoteMirror < ActiveRecord::Base
insecure_mode: true,
algorithm: 'aes-256-cbc'
- default_value_for :only_protected_branches, true
-
belongs_to :project, inverse_of: :remote_mirrors
validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true }
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 7769c3d71c0..b1d6d461928 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -85,6 +85,12 @@ class WikiPage
alias_method :to_param, :slug
+ def human_title
+ return 'Home' if title == 'home'
+
+ title
+ end
+
# The formatted title of this page.
def title
if @attributes[:title]
diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
index 2a337918d21..40aa9250885 100644
--- a/app/services/access_token_validation_service.rb
+++ b/app/services/access_token_validation_service.rb
@@ -6,6 +6,7 @@ class AccessTokenValidationService
EXPIRED = :expired
REVOKED = :revoked
INSUFFICIENT_SCOPE = :insufficient_scope
+ IMPERSONATION_DISABLED = :impersonation_disabled
attr_reader :token, :request
@@ -24,6 +25,11 @@ class AccessTokenValidationService
elsif !self.include_any_scope?(scopes)
return INSUFFICIENT_SCOPE
+ elsif token.respond_to?(:impersonation) &&
+ token.impersonation &&
+ !Gitlab.config.gitlab.impersonation_enabled
+ return IMPERSONATION_DISABLED
+
else
return VALID
end
diff --git a/app/services/ci/archive_trace_service.rb b/app/services/ci/archive_trace_service.rb
new file mode 100644
index 00000000000..a1dd00721b5
--- /dev/null
+++ b/app/services/ci/archive_trace_service.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Ci
+ class ArchiveTraceService
+ def execute(job)
+ job.trace.archive!
+ rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
+ # It's already archived, thus we can safely ignore this exception.
+ rescue => e
+ # Tracks this error with application logs, Sentry, and Prometheus.
+ # If `archive!` keeps failing for over a week, that could incur data loss.
+ # (See more https://docs.gitlab.com/ee/administration/job_traces.html#new-live-trace-architecture)
+ # In order to avoid interrupting the system, we do not raise an exception here.
+ archive_error(e, job)
+ end
+
+ private
+
+ def failed_archive_counter
+ @failed_archive_counter ||=
+ Gitlab::Metrics.counter(:job_trace_archive_failed_total,
+ "Counter of failed attempts of trace archiving")
+ end
+
+ def archive_error(error, job)
+ failed_archive_counter.increment
+ Rails.logger.error "Failed to archive trace. id: #{job.id} message: #{error.message}"
+
+ Gitlab::Sentry
+ .track_exception(error,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/51502',
+ extra: { job_id: job.id })
+ end
+ end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 92a8438ab2f..46a82377c10 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -4,6 +4,8 @@ module Ci
class CreatePipelineService < BaseService
attr_reader :pipeline
+ CreateError = Class.new(StandardError)
+
SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Build,
Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
@@ -47,6 +49,14 @@ module Ci
pipeline
end
+ def execute!(*args, &block)
+ execute(*args, &block).tap do |pipeline|
+ unless pipeline.persisted?
+ raise CreateError, pipeline.errors.full_messages.join(',')
+ end
+ end
+ end
+
private
def commit
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index c9d3ee31d82..927634c2159 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -8,6 +8,7 @@ module Files
transformer = Lfs::FileTransformer.new(project, @branch_name)
actions = actions_after_lfs_transformation(transformer, params[:actions])
+ actions = transform_move_actions(actions)
commit_actions!(actions)
end
@@ -26,6 +27,16 @@ module Files
end
end
+ # When moving a file, `content: nil` means "use the contents of the previous
+ # file", while `content: ''` means "move the file and set it to empty"
+ def transform_move_actions(actions)
+ actions.map do |action|
+ action[:infer_content] = true if action[:content].nil?
+
+ action
+ end
+ end
+
def commit_actions!(actions)
repository.multi_action(
current_user,
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 481de34b977..9e77a3237e3 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -9,7 +9,7 @@ module Projects
end
def execute
- if @params[:template_name]&.present?
+ if @params[:template_name].present?
return ::Projects::CreateFromTemplateService.new(current_user, params).execute
end
@@ -86,6 +86,8 @@ module Projects
@project.create_wiki unless skip_wiki?
end
+ @project.track_project_repository
+
event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create)
diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb
index 4462d504071..f3e026ba38c 100644
--- a/app/services/projects/hashed_storage/migrate_repository_service.rb
+++ b/app/services/projects/hashed_storage/migrate_repository_service.rb
@@ -30,6 +30,7 @@ module Projects
if result
project.write_repository_config
+ project.track_project_repository
else
rollback_folder_move
project.storage_version = nil
diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/admin/runners/_sort_dropdown.html.haml
index b201e6bf10e..19c2a50ebd9 100644
--- a/app/views/admin/runners/_sort_dropdown.html.haml
+++ b/app/views/admin/runners/_sort_dropdown.html.haml
@@ -1,7 +1,7 @@
- sorted_by = sort_options_hash[@sort]
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
= sorted_by
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index bfbc16d37a0..a733f420d11 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -8,7 +8,7 @@
%span.cred (Admin)
.float-right
- - if @user != current_user && @user.can?(:log_in)
+ - if impersonation_enabled? && @user != current_user && @user.can?(:log_in)
= link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
= link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index b694103ccaf..f518205f14c 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,8 +1,8 @@
- if current_user
.dropdown
- %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
- = icon('globe')
- %span.light= _("Visibility:")
+ %button.dropdown-menu-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' }
+ = icon('globe', class: 'mt-1')
+ %span.light.ml-3= _("Visibility:")
- if params[:visibility_level].present?
= visibility_level_label(params[:visibility_level].to_i)
- else
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 869c54d89ea..39d0f620283 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -37,6 +37,7 @@
.settings-content
= render 'shared/badges/badge_settings'
+= render_if_exists 'groups/custom_project_templates_setting'
= render_if_exists 'groups/templates_setting', expanded: expanded
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index c2bb1216c5f..30ab5781014 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -1,5 +1,5 @@
-- page_title "Invitation"
-%h3.page-title Invitation
+- page_title _("Invitation")
+%h3.page-title= _("Invitation")
%p
You have been invited
@@ -24,14 +24,17 @@
- if is_member
%p
- However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}.
- Sign in using a different account to accept the invitation.
+ - member_source = @member.source.is_a?(Group) ? _("group") : _("project")
+ = _("However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation.") % { member_source: member_source }
- if @member.invite_email != current_user.email
%p
- Note that this invitation was sent to #{mail_to @member.invite_email}, but you are signed in as #{link_to current_user.to_reference, user_url(current_user)} with email #{mail_to current_user.email}.
+ - mail_to_invite_email = mail_to(@member.invite_email)
+ - mail_to_current_user = mail_to(current_user.email)
+ - link_to_current_user = link_to(current_user.to_reference, user_url(current_user))
+ = _("Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}.").html_safe % { mail_to_invite_email: mail_to_invite_email, mail_to_current_user: mail_to_current_user, link_to_current_user: link_to_current_user }
- unless is_member
.actions
- = link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success"
- = link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
+ = link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn btn-success"
+ = link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index b44ea89510b..c63c34c4ebb 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -9,7 +9,7 @@
spellcheck: false, data: { 'filter-selector' => 'span.namespace-name' }
.dropdown
- %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.light sort:
- if @sort.present?
= sort_options_hash[@sort]
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 2f9bd5b04b6..dde0fae740b 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -32,7 +32,7 @@
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
.panel-footer
- = f.submit _('Mirror repository'), class: 'btn btn-success', name: :update_remote_mirror
+ = f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit', name: :update_remote_mirror
.panel.panel-default
.table-responsive
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 10e3b01096a..a760d02c4c3 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -50,7 +50,7 @@
.project-template
.form-group
%div
- = render 'project_templates', f: f
+ = render 'project_templates', f: f, project: @project
.tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
- if import_sources_enabled?
diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml
index 233c3adba0e..5b4d8927045 100644
--- a/app/views/projects/project_templates/_built_in_templates.html.haml
+++ b/app/views/projects/project_templates/_built_in_templates.html.haml
@@ -9,9 +9,9 @@
.text-muted
= template.description
.controls.d-flex.align-items-center
- %label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name }
+ %a.btn.btn-default.append-right-10{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
+ = _("Preview")
+ %label.btn.btn-success.template-button.choose-template.append-bottom-0{ for: template.name }
%input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } }
%span
= _("Use template")
- %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } }
- = _("Preview")
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 37535370940..026bc44a05f 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -14,7 +14,7 @@
= search_field_tag :search, params[:search], { placeholder: s_('TagsPage|Filter by tag name'), id: 'tag-search', class: 'form-control search-text-input input-short', spellcheck: false }
.dropdown
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown'} }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown'} }
%span.light
= tags_sort_options_hash[@sort]
= icon('chevron-down')
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 5e0523f0b96..889a13339fd 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,5 +1,5 @@
.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
- .table-holder
+ .table-holder.bordered-box
%table.table#tree-slider{ class: "table_#{@hex_path} tree-table qa-file-tree" }
%thead
%tr
diff --git a/app/views/projects/wikis/_sidebar_wiki_page.html.haml b/app/views/projects/wikis/_sidebar_wiki_page.html.haml
index 2423ac6abce..769d869bd53 100644
--- a/app/views/projects/wikis/_sidebar_wiki_page.html.haml
+++ b/app/views/projects/wikis/_sidebar_wiki_page.html.haml
@@ -1,3 +1,3 @@
%li{ class: active_when(params[:id] == wiki_page.slug) }
= link_to project_wiki_path(@project, wiki_page) do
- = wiki_page.title.capitalize
+ = wiki_page.human_title
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index aa9e29e5371..26671a7b7d2 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,7 +1,7 @@
- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, @page)
- breadcrumb_title @page.persisted? ? _("Edit") : _("New")
-- page_title @page.persisted? ? _("Edit") : _("New"), @page.title.capitalize, _("Wiki")
+- page_title @page.persisted? ? _("Edit") : _("New"), @page.human_title, _("Wiki")
= wiki_page_errors(@error)
@@ -12,9 +12,9 @@
.nav-text
%h2.wiki-page-title
- if @page.persisted?
- = link_to @page.title.capitalize, project_wiki_path(@project, @page)
+ = link_to @page.human_title, project_wiki_path(@project, @page)
- else
- = @page.title.capitalize
+ = @page.human_title
%span.light
&middot;
- if @page.persisted?
@@ -30,7 +30,7 @@
= link_to project_wiki_history_path(@project, @page), class: "btn" do
= s_("Wiki|Page history")
- if can?(current_user, :admin_wiki, @project)
- #delete-wiki-modal-wrapper{ data: { delete_wiki_url: project_wiki_path(@project, @page), page_title: @page.title.capitalize } }
+ #delete-wiki-modal-wrapper{ data: { delete_wiki_url: project_wiki_path(@project, @page), page_title: @page.human_title } }
= render 'form', uploads_path: wiki_attachment_upload_url
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 969a1677d9a..c5fbeeafa54 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,4 +1,4 @@
-- page_title _("History"), @page.title.capitalize, _("Wiki")
+- page_title _("History"), @page.human_title, _("Wiki")
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
@@ -6,7 +6,7 @@
.nav-text
%h2.wiki-page-title
- = link_to @page.title.capitalize, project_wiki_path(@project, @page)
+ = link_to @page.human_title, project_wiki_path(@project, @page)
%span.light
&middot;
= _("History")
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index fbf248c2058..cc38ec12fd8 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,7 +1,7 @@
- @content_class = "limit-container-width" unless fluid_layout
-- breadcrumb_title @page.title.capitalize
+- breadcrumb_title @page.human_title
- wiki_breadcrumb_dropdown_links(@page.slug)
-- page_title @page.title.capitalize, _("Wiki")
+- page_title @page.human_title, _("Wiki")
- add_to_breadcrumbs _("Wiki"), get_project_wiki_path(@project)
.wiki-page-header.has-sidebar-toggle
@@ -9,7 +9,7 @@
= icon('angle-double-left')
.nav-text
- %h2.wiki-page-title= @page.title.capitalize
+ %h2.wiki-page-title= @page.human_title
%span.wiki-last-edit-by
- if @page.last_version
= (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe
diff --git a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
index 6c4607b2f16..0d0a3c1aa64 100644
--- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
+++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
@@ -1,6 +1,6 @@
- if show_auto_devops_implicitly_enabled_banner?(project)
.auto-devops-implicitly-enabled-banner.alert.alert-warning
- - more_information_link = link_to _('More information'), 'https://docs.gitlab.com/ee/topics/autodevops/', class: 'alert-link'
+ - more_information_link = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link'
- auto_devops_message = s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}") % { more_information_link: more_information_link }
= auto_devops_message.html_safe
.alert-link-group
diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml
index a6ba3b59365..bd68a3e4c84 100644
--- a/app/views/shared/_milestones_sort_dropdown.html.haml
+++ b/app/views/shared/_milestones_sort_dropdown.html.haml
@@ -1,5 +1,5 @@
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
%span.light
- if @sort.present?
= milestone_sort_options_hash[@sort]
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index be6d4f1c32b..e4463c1e0d8 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -2,7 +2,7 @@
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
.dropdown.inline.prepend-left-10
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
= sorted_by
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 2237b93a10b..1ae6d1f5ee3 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -9,7 +9,7 @@
- default_sort_by = sort_value_recently_created
.dropdown.inline.js-group-filter-dropdown-wrap.append-right-10
- %button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.dropdown-label
= options_hash[default_sort_by]
= icon('chevron-down')
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 6939aba6896..7c5af0b9775 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -95,7 +95,10 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
- = _('No Label')
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
diff --git a/app/views/shared/labels/_sort_dropdown.html.haml b/app/views/shared/labels/_sort_dropdown.html.haml
index 8a7d037e15b..d664ef1cc2f 100644
--- a/app/views/shared/labels/_sort_dropdown.html.haml
+++ b/app/views/shared/labels/_sort_dropdown.html.haml
@@ -1,6 +1,6 @@
- sort_title = label_sort_options_hash[@sort] || sort_title_name_desc
.dropdown.inline
- %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
= sort_title
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort
diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb
index c1283e9b2fc..4a9becf0ca7 100644
--- a/app/workers/archive_trace_worker.rb
+++ b/app/workers/archive_trace_worker.rb
@@ -7,7 +7,7 @@ class ArchiveTraceWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(job_id)
Ci::Build.without_archived_trace.find_by(id: job_id).try do |job|
- job.trace.archive!
+ Ci::ArchiveTraceService.new.execute(job)
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
index 7443aad1380..f65ff239866 100644
--- a/app/workers/ci/archive_traces_cron_worker.rb
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -11,21 +11,9 @@ module Ci
# This could happen when ArchiveTraceWorker sidekiq jobs were lost by receiving SIGKILL
# More details in https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build|
- begin
- build.trace.archive!
- rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
- rescue => e
- failed_archive_counter.increment
- Rails.logger.error "Failed to archive stale live trace. id: #{build.id} message: #{e.message}"
- end
+ Ci::ArchiveTraceService.new.execute(build)
end
end
# rubocop: enable CodeReuse/ActiveRecord
-
- private
-
- def failed_archive_counter
- @failed_archive_counter ||= Gitlab::Metrics.counter(:job_trace_archive_failed_total, "Counter of failed attempts of traces archiving")
- end
end
end
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 85d1ffe0fa9..ac4e9710f33 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -9,18 +9,36 @@ class PipelineScheduleWorker
Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
.preload(:owner, :project).find_each do |schedule|
begin
- pipeline = Ci::CreatePipelineService.new(schedule.project,
- schedule.owner,
- ref: schedule.ref)
- .execute(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
-
- schedule.deactivate! unless pipeline.persisted?
+ Ci::CreatePipelineService.new(schedule.project,
+ schedule.owner,
+ ref: schedule.ref)
+ .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule)
rescue => e
- Rails.logger.error "#{schedule.id}: Failed to create a scheduled pipeline: #{e.message}"
+ error(schedule, e)
ensure
schedule.schedule_next_run!
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def error(schedule, error)
+ failed_creation_counter.increment
+
+ Rails.logger.error "Failed to create a scheduled pipeline. " \
+ "schedule_id: #{schedule.id} message: #{error.message}"
+
+ Gitlab::Sentry
+ .track_exception(error,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: schedule.id })
+ end
+
+ def failed_creation_counter
+ @failed_creation_counter ||=
+ Gitlab::Metrics.counter(:pipeline_schedule_creation_failed_total,
+ "Counter of failed attempts of pipeline schedule creation")
+ end
end
diff --git a/changelogs/unreleased/1979-redesign-mr-widget-approvals-ce.yml b/changelogs/unreleased/1979-redesign-mr-widget-approvals-ce.yml
new file mode 100644
index 00000000000..d05b6054b22
--- /dev/null
+++ b/changelogs/unreleased/1979-redesign-mr-widget-approvals-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Redesign of MR header sections (CE)
+merge_request: 23465
+author:
+type: changed
diff --git a/changelogs/unreleased/40385-prohibit_impersonation.yml b/changelogs/unreleased/40385-prohibit_impersonation.yml
new file mode 100644
index 00000000000..dd061b17939
--- /dev/null
+++ b/changelogs/unreleased/40385-prohibit_impersonation.yml
@@ -0,0 +1,5 @@
+---
+title: Add config to prohibit impersonation
+merge_request: 23338
+author:
+type: added
diff --git a/changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml b/changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml
new file mode 100644
index 00000000000..cfc74bef638
--- /dev/null
+++ b/changelogs/unreleased/48496-merge-request-refactor-does-not-highlight-selected-line.yml
@@ -0,0 +1,5 @@
+---
+title: When user clicks linenumber in MR changes, highlight that line
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/50264-add-border-around-the-repository-file-tree.yml b/changelogs/unreleased/50264-add-border-around-the-repository-file-tree.yml
new file mode 100644
index 00000000000..6315c3e7f36
--- /dev/null
+++ b/changelogs/unreleased/50264-add-border-around-the-repository-file-tree.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Add border around the repository file tree
+merge_request: 23018
+author:
+type: changed
diff --git a/changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml b/changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml
new file mode 100644
index 00000000000..dc11ede5c8d
--- /dev/null
+++ b/changelogs/unreleased/51029-status-emoji-currently-replaces-avatar-on-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve status emoji being replaced by avatar on mobile
+merge_request: 23408
+author:
+type: other
diff --git a/changelogs/unreleased/51083-fix-move-api.yml b/changelogs/unreleased/51083-fix-move-api.yml
new file mode 100644
index 00000000000..8838f6f267e
--- /dev/null
+++ b/changelogs/unreleased/51083-fix-move-api.yml
@@ -0,0 +1,5 @@
+---
+title: 'Commits API: Preserve file content in move operations if unspecified'
+merge_request: 23387
+author:
+type: fixed
diff --git a/changelogs/unreleased/52370-filter-by-none-any-for-labels-in-issues-mrs-boards.yml b/changelogs/unreleased/52370-filter-by-none-any-for-labels-in-issues-mrs-boards.yml
new file mode 100644
index 00000000000..9e1ee3ede5e
--- /dev/null
+++ b/changelogs/unreleased/52370-filter-by-none-any-for-labels-in-issues-mrs-boards.yml
@@ -0,0 +1,5 @@
+---
+title: Adds Any option to label filters
+merge_request: 23111
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/53400-unstar-icon-button-is-misaligned.yml b/changelogs/unreleased/53400-unstar-icon-button-is-misaligned.yml
new file mode 100644
index 00000000000..b393795f491
--- /dev/null
+++ b/changelogs/unreleased/53400-unstar-icon-button-is-misaligned.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix: Unstar icon button is misaligned'
+merge_request: 23444
+author:
+type: fixed
diff --git a/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml b/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml
deleted file mode 100644
index 44362a8622e..00000000000
--- a/changelogs/unreleased/53763-fix-encrypt-columns-data-loss.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Correctly handle data-loss scenarios when encrypting columns
-merge_request: 23306
-author:
-type: fixed
diff --git a/changelogs/unreleased/53778-remove-site-statistics.yml b/changelogs/unreleased/53778-remove-site-statistics.yml
deleted file mode 100644
index fe006e43671..00000000000
--- a/changelogs/unreleased/53778-remove-site-statistics.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Removed Site Statistics optimization as it was causing problems
-merge_request: 23314
-author:
-type: removed
diff --git a/changelogs/unreleased/54048-Line-numbers-are-misaligned-in-file-blame-view.yml b/changelogs/unreleased/54048-Line-numbers-are-misaligned-in-file-blame-view.yml
new file mode 100644
index 00000000000..8ceac4ec869
--- /dev/null
+++ b/changelogs/unreleased/54048-Line-numbers-are-misaligned-in-file-blame-view.yml
@@ -0,0 +1,5 @@
+---
+title: Fix line height of numbers in file blame view
+merge_request: 23090
+author: Johann Hubert Sonntagbauer
+type: fixed
diff --git a/changelogs/unreleased/54218-fix-mergeUrlParams.yml b/changelogs/unreleased/54218-fix-mergeUrlParams.yml
new file mode 100644
index 00000000000..dae06b66e8e
--- /dev/null
+++ b/changelogs/unreleased/54218-fix-mergeUrlParams.yml
@@ -0,0 +1,5 @@
+---
+title: "Fix mergeUrlParams with fragment URL"
+merge_request: 54218
+author: Thomas Holder
+type: fixed
diff --git a/changelogs/unreleased/54282-tooltip-stuck.yml b/changelogs/unreleased/54282-tooltip-stuck.yml
deleted file mode 100644
index 655870499a0..00000000000
--- a/changelogs/unreleased/54282-tooltip-stuck.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixes stuck tooltip on stop env button
-merge_request: 23244
-author:
-type: fixed
diff --git a/changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml b/changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml
new file mode 100644
index 00000000000..671d1590991
--- /dev/null
+++ b/changelogs/unreleased/54648-fix-order-by-dropdown-tablet-screens.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Order By dropdown menu styling in tablet and mobile screens
+merge_request: 23446
+author:
+type: fixed
diff --git a/changelogs/unreleased/ab-approximate-counts.yml b/changelogs/unreleased/ab-approximate-counts.yml
new file mode 100644
index 00000000000..8a67239d031
--- /dev/null
+++ b/changelogs/unreleased/ab-approximate-counts.yml
@@ -0,0 +1,5 @@
+---
+title: Approximate counting strategy with TABLESAMPLE.
+merge_request: 22650
+author:
+type: performance
diff --git a/changelogs/unreleased/bump_gpgme_gem.yml b/changelogs/unreleased/bump_gpgme_gem.yml
new file mode 100644
index 00000000000..4c0067cb824
--- /dev/null
+++ b/changelogs/unreleased/bump_gpgme_gem.yml
@@ -0,0 +1,5 @@
+---
+title: Bump gpgme gem version from 2.0.13 to 2.0.18
+merge_request:
+author: asaparov
+type: other
diff --git a/changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml b/changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml
new file mode 100644
index 00000000000..b5fd99c304f
--- /dev/null
+++ b/changelogs/unreleased/ce-52811-fix_namespaces_api_routing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix API::Namespaces routing to accept namepaces with dots
+merge_request: 22912
+author:
+type: fixed
diff --git a/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml b/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml
deleted file mode 100644
index 6cc743d6f3a..00000000000
--- a/changelogs/unreleased/ce-53347_fix_impersonation_tokens.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Display impersonation token value only after creation
-merge_request: 22916
-author:
-type: fixed
diff --git a/changelogs/unreleased/dm-batch-loader-sidekiq.yml b/changelogs/unreleased/dm-batch-loader-sidekiq.yml
deleted file mode 100644
index 87936dc2603..00000000000
--- a/changelogs/unreleased/dm-batch-loader-sidekiq.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clear BatchLoader context between Sidekiq jobs
-merge_request: 23308
-author:
-type: fixed
diff --git a/changelogs/unreleased/document-raw-snippet-api.yml b/changelogs/unreleased/document-raw-snippet-api.yml
new file mode 100644
index 00000000000..3b8818cea5c
--- /dev/null
+++ b/changelogs/unreleased/document-raw-snippet-api.yml
@@ -0,0 +1,5 @@
+---
+title: Fix lack of documentation on how to fetch a snippet's content using API
+merge_request: 23448
+author: Colin Leroy
+type: other
diff --git a/changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml b/changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml
new file mode 100644
index 00000000000..ab926fbd43b
--- /dev/null
+++ b/changelogs/unreleased/fix-mr-widget-unrelated-deployment-status.yml
@@ -0,0 +1,5 @@
+---
+title: Fix unrelated deployment status in MR widget
+merge_request: 23175
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-not-render-emoji.yml b/changelogs/unreleased/fix-not-render-emoji.yml
deleted file mode 100644
index 857b97004f0..00000000000
--- a/changelogs/unreleased/fix-not-render-emoji.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix not render emoji in filter dropdown
-merge_request: 23112
-author: Hiroyuki Sato
-type: fixed
diff --git a/changelogs/unreleased/gt-externalize-app-views-invites.yml b/changelogs/unreleased/gt-externalize-app-views-invites.yml
new file mode 100644
index 00000000000..b5a22177f9b
--- /dev/null
+++ b/changelogs/unreleased/gt-externalize-app-views-invites.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings from `/app/views/invites`
+merge_request: 23205
+author: Tao Wang
+type: other
diff --git a/changelogs/unreleased/ignore-failed-pipeline-creation-on-pipeline-schedule.yml b/changelogs/unreleased/ignore-failed-pipeline-creation-on-pipeline-schedule.yml
new file mode 100644
index 00000000000..90f47aa12db
--- /dev/null
+++ b/changelogs/unreleased/ignore-failed-pipeline-creation-on-pipeline-schedule.yml
@@ -0,0 +1,5 @@
+---
+title: Remove auto deactivation when failed to create a pipeline via pipeline schedules
+merge_request: 22243
+author:
+type: changed
diff --git a/changelogs/unreleased/improve_auto_devops_migration_debug.yml b/changelogs/unreleased/improve_auto_devops_migration_debug.yml
new file mode 100644
index 00000000000..96a78808361
--- /dev/null
+++ b/changelogs/unreleased/improve_auto_devops_migration_debug.yml
@@ -0,0 +1,6 @@
+---
+title: 'Auto DevOps: Add echo for each branch of the deploy() function where we run
+ helm upgrade'
+merge_request: 23499
+author:
+type: changed
diff --git a/changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml b/changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml
new file mode 100644
index 00000000000..d21254b16d0
--- /dev/null
+++ b/changelogs/unreleased/jivl-add-empty-state-graphs-null-values.yml
@@ -0,0 +1,5 @@
+---
+title: Add empty state for graphs with no values
+merge_request: 22630
+author:
+type: fixed
diff --git a/changelogs/unreleased/jupyter-tls.yml b/changelogs/unreleased/jupyter-tls.yml
new file mode 100644
index 00000000000..4111edd34ff
--- /dev/null
+++ b/changelogs/unreleased/jupyter-tls.yml
@@ -0,0 +1,5 @@
+---
+title: "#52753: HTTPS for JupyterHub installation"
+merge_request: 23479
+author: Amit Rathi
+type: added
diff --git a/changelogs/unreleased/non-webkit-scrollbar-fixing.yml b/changelogs/unreleased/non-webkit-scrollbar-fixing.yml
new file mode 100644
index 00000000000..526a9f25486
--- /dev/null
+++ b/changelogs/unreleased/non-webkit-scrollbar-fixing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix horizontal scrollbar overlapping on horizontal scrolling-tabs
+merge_request: 23167
+author: Harry Kiselev
+type: other
diff --git a/changelogs/unreleased/order-of-notification-settings.yml b/changelogs/unreleased/order-of-notification-settings.yml
new file mode 100644
index 00000000000..0f0243bcb40
--- /dev/null
+++ b/changelogs/unreleased/order-of-notification-settings.yml
@@ -0,0 +1,5 @@
+---
+title: reorder notification settings by noisy-ness
+merge_request:
+author: C.J. Jameson
+type: changed
diff --git a/changelogs/unreleased/render-text-deprecated.yml b/changelogs/unreleased/render-text-deprecated.yml
new file mode 100644
index 00000000000..7dbbd13bcef
--- /dev/null
+++ b/changelogs/unreleased/render-text-deprecated.yml
@@ -0,0 +1,6 @@
+---
+title: 'Fix deprecation: render :text is deprecated because it does not actually render
+ a text/plain response'
+merge_request: 23425
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/sh-fix-hash-filename-handling.yml b/changelogs/unreleased/sh-fix-hash-filename-handling.yml
deleted file mode 100644
index cb32051a4ab..00000000000
--- a/changelogs/unreleased/sh-fix-hash-filename-handling.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix handling of filenames with hash characters in tree view
-merge_request: 23368
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-38317.yml b/changelogs/unreleased/sh-fix-issue-38317.yml
new file mode 100644
index 00000000000..13fcb5b8f96
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-38317.yml
@@ -0,0 +1,5 @@
+---
+title: Remove needless auto-capitalization on Wiki page titles
+merge_request: 23288
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-mirrors-protected-branches.yml b/changelogs/unreleased/sh-fix-mirrors-protected-branches.yml
new file mode 100644
index 00000000000..627de25650d
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-mirrors-protected-branches.yml
@@ -0,0 +1,5 @@
+---
+title: Fix "protected branches only" checkbox not set properly at init
+merge_request: 23409
+author:
+type: fixed
diff --git a/changelogs/unreleased/tc-repo-full-path-in-db.yml b/changelogs/unreleased/tc-repo-full-path-in-db.yml
new file mode 100644
index 00000000000..ead8feabeb9
--- /dev/null
+++ b/changelogs/unreleased/tc-repo-full-path-in-db.yml
@@ -0,0 +1,5 @@
+---
+title: Add model and relation to store repo full path in database
+merge_request: 23143
+author:
+type: added
diff --git a/changelogs/unreleased/winh-merge-request-commit-discussion.yml b/changelogs/unreleased/winh-merge-request-commit-discussion.yml
new file mode 100644
index 00000000000..b0c6264369b
--- /dev/null
+++ b/changelogs/unreleased/winh-merge-request-commit-discussion.yml
@@ -0,0 +1,5 @@
+---
+title: Display commit ID for commit diff discussion on merge request
+merge_request: 23370
+author:
+type: fixed
diff --git a/changelogs/unreleased/workhorse-7-3-0.yml b/changelogs/unreleased/workhorse-7-3-0.yml
new file mode 100644
index 00000000000..6708b8a3cbb
--- /dev/null
+++ b/changelogs/unreleased/workhorse-7-3-0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade GitLab Workhorse to v7.3.0
+merge_request: 23489
+author:
+type: other
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 09e21b2c6f2..58b7c248aaf 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -114,6 +114,9 @@ production: &base
# The default is 'shared/cache/archive/' relative to the root of the Rails app.
# repository_downloads_path: shared/cache/archive/
+ ## Impersonation settings
+ impersonation_enabled: true
+
## Reply by email
# Allow users to comment on issues and merge requests by replying to notification emails.
# For documentation on how to set this up, see http://doc.gitlab.com/ce/administration/reply_by_email.html
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index bd02b85c7ce..82e3b490378 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -153,6 +153,7 @@ Settings.gitlab['domain_whitelist'] ||= []
Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values
Settings.gitlab['trusted_proxies'] ||= []
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
+Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
#
diff --git a/db/migrate/20181122160027_create_project_repositories.rb b/db/migrate/20181122160027_create_project_repositories.rb
new file mode 100644
index 00000000000..e42cef9b1c6
--- /dev/null
+++ b/db/migrate/20181122160027_create_project_repositories.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateProjectRepositories < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :project_repositories, id: :bigserial do |t|
+ t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict }
+ t.string :disk_path, null: false, index: { unique: true }
+ t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ end
+ end
+end
diff --git a/db/migrate/20181123042307_drop_site_statistics.rb b/db/post_migrate/20181123042307_drop_site_statistics.rb
index 8986374ef65..8986374ef65 100644
--- a/db/migrate/20181123042307_drop_site_statistics.rb
+++ b/db/post_migrate/20181123042307_drop_site_statistics.rb
diff --git a/db/schema.rb b/db/schema.rb
index 2e9b2a9ac89..5627d389677 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1606,6 +1606,15 @@ ActiveRecord::Schema.define(version: 20181126153547) do
t.index ["status"], name: "index_project_mirror_data_on_status", using: :btree
end
+ create_table "project_repositories", id: :bigserial, force: :cascade do |t|
+ t.integer "shard_id", null: false
+ t.string "disk_path", null: false
+ t.integer "project_id", null: false
+ t.index ["disk_path"], name: "index_project_repositories_on_disk_path", unique: true, using: :btree
+ t.index ["project_id"], name: "index_project_repositories_on_project_id", unique: true, using: :btree
+ t.index ["shard_id"], name: "index_project_repositories_on_shard_id", using: :btree
+ end
+
create_table "project_statistics", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "namespace_id", null: false
@@ -2392,6 +2401,8 @@ ActiveRecord::Schema.define(version: 20181126153547) do
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
add_foreign_key "project_mirror_data", "projects", on_delete: :cascade
+ add_foreign_key "project_repositories", "projects", on_delete: :cascade
+ add_foreign_key "project_repositories", "shards", on_delete: :restrict
add_foreign_key "project_statistics", "projects", on_delete: :cascade
add_foreign_key "projects", "pool_repositories", name: "fk_6e5c14658a", on_delete: :nullify
add_foreign_key "prometheus_metrics", "projects", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index ba2bb89533b..b15c3a63d92 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -3,6 +3,12 @@ comments: false
description: 'Learn how to use and administer GitLab, the most scalable Git-based fully integrated platform for software development.'
---
+<div class="display-none">
+ <em>Visit <a href="https://docs.gitlab.com/ce/">docs.gitlab.com</a> for optimized
+ navigation, discoverability, and readability.</em>
+</div>
+<!-- the div above will not display on the docs site but will display on /help -->
+
# GitLab Documentation
Welcome to [GitLab](https://about.gitlab.com/) Documentation.
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 3395c503ced..dc6a71e2ebd 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -237,3 +237,14 @@ gitaly_enabled=false
When you run `service gitlab restart` Gitaly will be disabled on this
particular machine.
+
+## Troubleshooting Gitaly in production
+
+Since GitLab 11.6, Gitaly comes with a command-line tool called
+`gitaly-debug` that can be run on a Gitaly server to aid in
+troubleshooting. In GitLab 11.6 its only sub-command is
+`simulate-http-clone` which allows you to measure the maximum possible
+Git clone speed for a specific repository on the server.
+
+For an up to date list of sub-commands see [the gitaly-debug
+README](https://gitlab.com/gitlab-org/gitaly/blob/master/cmd/gitaly-debug/README.md).
diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md
index c358dfbead2..dfd9be3d04c 100644
--- a/doc/administration/monitoring/performance/request_profiling.md
+++ b/doc/administration/monitoring/performance/request_profiling.md
@@ -1,16 +1,17 @@
# Request Profiling
## Procedure
+
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>` to the request you want to profile. You can use any of these tools
- * [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension
- * [Modify Headers](https://addons.mozilla.org/en-US/firefox/addon/modify-headers/) Firefox extension
- * `curl --header 'X-Profile-Token: <token>' https://gitlab.example.com/group/project`
+ (highlighted in a blue in the image below).
+ ![Profile token](img/request_profiling_token.png)
+1. Pass the header `X-Profile-Token: <token>` 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>' https://gitlab.example.com/group/project`.
1. Once request is finished (which will take a little longer than usual), you can
-view the profiling output from `Monitoring > Requests Profiles` admin page.
-![Profiling output](img/request_profile_result.png)
+ view the profiling output from `Monitoring > Requests Profiles` admin page.
+ ![Profiling output](img/request_profile_result.png)
## Cleaning up
+
Profiling output will be cleared out every day via a Sidekiq worker.
diff --git a/doc/api/README.md b/doc/api/README.md
index 0e8d1aaf86e..b49c3a198f1 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -225,6 +225,42 @@ For more information, refer to the
Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the
`private_token` parameter or the `Private-Token` header.
+#### Disable impersonation
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/40385) in GitLab
+11.6.
+
+By default, impersonation is enabled. To disable impersonation:
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_rails['impersonation_enabled'] = false
+ ```
+
+1. Save the file and [reconfigure](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure)
+ GitLab for the changes to take effect.
+
+To re-enable impersonation, remove this configuration and reconfigure GitLab.
+
+---
+
+**For installations from source**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ gitlab:
+ impersonation_enabled: false
+ ```
+
+1. Save the file and [restart](../administration/restart_gitlab.md#installations-from-source)
+ GitLab for the changes to take effect.
+
+To re-enable impersonation, remove this configuration and restart GitLab.
+
### Sudo
NOTE: **Note:**
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 7d9b52ec24f..6c16216429d 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -87,7 +87,7 @@ POST /projects/:id/repository/commits
| `action` | string | yes | The action to perform, `create`, `delete`, `move`, `update`, `chmod`|
| `file_path` | string | yes | Full path to the file. Ex. `lib/class.rb` |
| `previous_path` | string | no | Original full path to the file being moved. Ex. `lib/class1.rb`. Only considered for `move` action. |
-| `content` | string | no | File content, required for all except `delete` and `chmod`. Optional for `move` |
+| `content` | string | no | File content, required for all except `delete`, `chmod`, and `move`. Move actions that do not specify `content` will preserve the existing file content, and any other value of `content` will overwrite the file content. |
| `encoding` | string | no | `text` or `base64`. `text` is default. |
| `last_commit_id` | string | no | Last known file commit id. Will be only considered in update, move and delete actions. |
| `execute_filemode` | boolean | no | When `true/false` enables/disables the execute flag on the file. Only considered for `chmod` action. |
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index c8de7f2191d..55f5a4cc3b2 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -108,14 +108,18 @@ Get an archive of the repository. This endpoint can be accessed without
authentication if the repository is publicly accessible.
```
-GET /projects/:id/repository/archive
+GET /projects/:id/repository/archive[.format]
```
+`format` is an optional suffix for the archive format. Default is
+`tar.gz`. Options are `tar.gz`, `tar.bz2`, `tbz`, 'tbz2`, `tb2`,
+`bz2`, `tar`, and `zip`. For example, specifying `archive.zip`
+would send an archive in ZIP format.
+
Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `sha` (optional) - The commit SHA to download. A tag, branch reference or sha can be used. This defaults to the tip of the default branch if not specified
-- `format` (optional) - The archive format. Default is `tar.gz`. Options are `tar.gz`, `tar.bz2`, `tbz`, `tbz2`, `tb2`, `bz2`, `tar`, `zip`
## Compare branches, tags or commits
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
index 7892866cd8e..e840e640377 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -37,13 +37,13 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
-``` bash
+```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/1
```
Example response:
-``` json
+```json
{
"id": 1,
"title": "test",
@@ -65,6 +65,30 @@ Example response:
}
```
+## Single snippet contents
+
+Get a single snippet's raw contents.
+
+```
+GET /snippets/:id/raw
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | Integer | yes | The ID of a snippet |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/1/raw
+```
+
+Example response:
+
+```
+Hello World snippet
+```
+
## Create new snippet
Creates a new snippet. The user must have permission to create new snippets.
@@ -84,7 +108,7 @@ Parameters:
| `visibility` | String | no | The snippet's visibility |
-``` bash
+```bash
curl --request POST \
--data '{"title": "This is a snippet", "content": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \
--header 'Content-Type: application/json' \
@@ -94,7 +118,7 @@ curl --request POST \
Example response:
-``` json
+```json
{
"id": 1,
"title": "This is a snippet",
@@ -136,7 +160,7 @@ Parameters:
| `visibility` | String | no | The snippet's visibility |
-``` bash
+```bash
curl --request PUT \
--data '{"title": "foo", "content": "bar"}' \
--header 'Content-Type: application/json' \
@@ -146,7 +170,7 @@ curl --request PUT \
Example response:
-``` json
+```json
{
"id": 1,
"title": "test",
@@ -201,13 +225,13 @@ GET /snippets/public
| `per_page` | Integer | no | number of snippets to return per page |
| `page` | Integer | no | the page to retrieve |
-``` bash
+```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/public?per_page=2&page=1
```
Example response:
-``` json
+```json
[
{
"author": {
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index b090ea014dc..b1ccce744d8 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -273,6 +273,8 @@ The `releases` directory will hold all our deployments:
echo 'Cloning repository'
[ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
+ cd {{ $releases_dir }}
+ git reset --hard {{ $commit }}
@endtask
...
@@ -349,6 +351,8 @@ At the end, our `Envoy.blade.php` file will look like this:
echo 'Cloning repository'
[ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
+ cd {{ $releases_dir }}
+ git reset --hard {{ $commit }}
@endtask
@task('run_composer')
@@ -519,7 +523,7 @@ deploy_production:
- mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- - ~/.composer/vendor/bin/envoy run deploy
+ - ~/.composer/vendor/bin/envoy run deploy --commit="$CI_COMMIT_SHA"
environment:
name: production
url: http://192.168.1.1
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index e46b2bbc79c..af7e41db443 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1590,7 +1590,7 @@ Possible values for `when` are:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22631) in GitLab 11.5.
`parallel` allows you to configure how many instances of a job to run in
-parallel. This value has to be greater than or equal to two (2) and less or equal than 50.
+parallel. This value has to be greater than or equal to two (2) and less than or equal to 50.
This creates N instances of the same job that run in parallel. They're named
sequentially from `job_name 1/N` to `job_name N/N`.
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 931a7a8e6d5..e65c5f05505 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -55,7 +55,7 @@ GitLab can be considered to have two layers from a process perspective:
### gitaly
-- [Omnibus confiugration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration)
+- [Omnibus configuration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration)
- Layer: Core Service (Data)
Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab. (Think GitLab.com or High Availablity Deployments) As of 11.3.0, This service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly).
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index df2cb30c5d6..7788d155154 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -53,6 +53,8 @@ from teams other than your own.
#### Security requirements
+ 1. If your merge request is processing, storing, or transferring any kind of [RED or ORANGE data](https://docs.google.com/document/d/15eNKGA3zyZazsJMldqTBFbYMnVUSQSpU14lo22JMZQY/edit) (this is a confidential document), it must be
+ **approved by a [Security Engineer][team]**.
1. If your merge request involves implementing, utilizing, or is otherwise related to any type of authentication, authorization, or session handling mechanism, it must be
**approved by a [Security Engineer][team]**.
1. If your merge request has a goal which requires a cryptographic function such as: confidentiality, integrity, authentication, or non-repudiation, it must be
@@ -85,6 +87,23 @@ If an author is unsure if a merge request needs a domain expert's opinion, that'
usually a pretty good sign that it does, since without it the required level of
confidence in their solution will not have been reached.
+Before the review, the author is requested to submit comments on the merge
+request diff alerting the reviewer to anything important as well as for anything
+that demands further explanation or attention. Examples of content that may
+warrant a comment could be:
+
+- The addition of a linting rule (Rubocop, JS etc)
+- The addition of a library (Ruby gem, JS lib etc)
+- Where not obvious, a link to the parent class or method
+- Any benchmarking performed to complement the change
+- Potentially insecure code
+
+Do not add these comments directly to the source code, unless the
+reviewer requires you to do so.
+
+This
+[saves reviewers time and helps authors catch mistakes earlier](https://www.ibm.com/developerworks/rational/library/11-proven-practices-for-peer-review/index.html#__RefHeading__97_174136755).
+
### The responsibility of the maintainer
Maintainers are responsible for the overall health, quality, and consistency of
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index a227e2f6640..790b1bf951b 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -511,7 +511,7 @@ module EE
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: ::API::API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
# ...
end
end
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
new file mode 100644
index 00000000000..f55f01720fd
--- /dev/null
+++ b/doc/development/fe_guide/graphql.md
@@ -0,0 +1,83 @@
+# GraphQL
+
+We use [Apollo] and [Vue Apollo][vue-apollo] for working with GraphQL
+on the frontend.
+
+In order to use GraphQL, you need to enable the `graphql` feature flag,
+read more about [Feature Flags][feature-flags].
+
+## Apollo Client
+
+To save duplicated clients getting created in different apps, we have a
+[default client][defualt-client] that should be used. This setups the
+Apollo client with the correct URL and also sets the CSRF headers.
+
+## GraphQL Queries
+
+To save query compilation at runtime, webpack can directly import `.graphql`
+files. This allows webpack to preprocess the query at compile time instead
+of the client doing compilation of queries.
+
+## Usage in Vue
+
+To use Vue Apollo, import the [Vue Apollo][vue-apollo] plugin as well
+as the default client. This should be created at the same point
+the Vue application is mounted.
+
+```javascript
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import defaultClient from '~/lib/graphql';
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient,
+});
+
+new Vue({
+ ...,
+ apolloProvider,
+ ...
+});
+```
+
+Read more about [Vue Apollo][vue-apollo] in the [Vue Apollo documentation][vue-apollo-docs].
+
+### Testing
+
+With [Vue test utils][vue-test-utils] it is easy to quickly test components that
+fetch GraphQL queries. The simplest way is to use `shallowMount` and then set
+the data on the component
+
+```javascript
+it('tests apollo component', () => {
+ const vm = shallowMount(App);
+
+ vm.setData({
+ ...mock data
+ });
+});
+```
+
+## Usage outside of Vue
+
+It is also possible to use GraphQL outside of Vue by directly importing
+and using the default client with queries.
+
+```javascript
+import defaultClient from '~/lib/graphql';
+import query from './query.graphql';
+
+defaultClient.query(query)
+ .then(result => console.log(result));
+```
+
+Read more about the [Apollo] client in the [Apollo documentation][apollo-client-docs].
+
+[Apollo]: https://www.apollographql.com/
+[vue-apollo]: https://github.com/Akryum/vue-apollo/
+[vue-apollo-docs]: https://akryum.github.io/vue-apollo/
+[feature-flags]: ../feature_flags.md
+[default-client]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/lib/graphql.js
+[apollo-client-docs]: https://www.apollographql.com/docs/tutorial/client.html
+[vue-test-utils]: https://vue-test-utils.vuejs.org/
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 11b9a2e6a64..cca3ad6fae6 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -54,6 +54,9 @@ Vuex specific design patterns and practices.
## [Axios](axios.md)
Axios specific practices and gotchas.
+## [GraphQL](graphql.md)
+How to use GraphQL
+
## [Icons and Illustrations](icons.md)
How we use SVG for our Icons and Illustrations.
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 394f3ea60b7..766a23c419d 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -44,6 +44,5 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Kanboard Plugin GitLab Authentication](https://github.com/kanboard/plugin-gitlab-auth)
- [Jenkins GitLab OAuth Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+OAuth+Plugin)
-- [Set up Gitlab CE with Active Directory authentication](https://www.caseylabs.com/setup-gitlab-ce-with-active-directory-authentication/)
- [How to customize GitLab to support OpenID authentication](http://eric.van-der-vlist.com/blog/2013/11/23/how-to-customize-gitlab-to-support-openid-authentication/)
- [Openshift - Configuring Authentication and User Agent](https://docs.openshift.org/latest/install_config/configuring_authentication.html#GitLab)
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 6bb2e236dc1..a8b088395aa 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -479,14 +479,23 @@ no longer be valid as soon as the deployment job finishes. This means that
Kubernetes can run the application, but in case it should be restarted or
executed somewhere else, it cannot be accessed again.
+#### Migrations
+
> [Introduced][ce-21955] in GitLab 11.4
Database initialization and migrations for PostgreSQL can be configured to run
within the application pod by setting the project variables `DB_INITIALIZE` and
`DB_MIGRATE` respectively.
-If present, `DB_INITIALIZE` will be run as a shell command within an application pod as a helm
-post-install hook. Note that this means that if any deploy succeeds,
+If present, `DB_INITIALIZE` will be run as a shell command within an
+application pod as a helm post-install hook. As some applications will
+not run without a successful database initialization step, GitLab will
+deploy the first release without the application deployment and only the
+database initialization step. After the database initialization completes,
+GitLab will deploy a second release with the application deployment as
+normal.
+
+Note that a post-install hook means that if any deploy succeeds,
`DB_INITIALIZE` will not be processed thereafter.
If present, `DB_MIGRATE` will be run as a shell command within an application pod as
diff --git a/doc/user/admin_area/custom_project_templates.md b/doc/user/admin_area/custom_project_templates.md
new file mode 100644
index 00000000000..5afbf9f2934
--- /dev/null
+++ b/doc/user/admin_area/custom_project_templates.md
@@ -0,0 +1,25 @@
+# Custom instance-level project templates **[PREMIUM ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6860) in [GitLab Premium](https://about.gitlab.com/pricing) 11.2.
+
+When you create a new project, creating it based on custom project templates is
+a convenient option to bootstrap from an existing project boilerplate.
+The administration setting to configure a GitLab group that serves as template
+source can be found under **Admin > Settings > Custom project templates**.
+
+Within this section, you can configure the group where all the custom project
+templates are sourced. Every project directly under the group namespace will be
+available to the user if they have access to them. For example, every public
+project in the group will be available to every logged user. However,
+private projects will be available only if the user has view [permissions](../permissions.md)
+in the project:
+
+- Project Owner, Maintainer, Developer, Reporter or Guest
+- Is a member of the Group: Owner, Maintainer, Developer, Reporter or Guest
+
+Projects below subgroups of the template group are **not** supported.
+
+Repository and database information that are copied over to each new project are
+identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md).
+
+If you would like to set project templates at a group level, please see [Custom group-level project templates](../group/custom_project_templates.md). \ No newline at end of file
diff --git a/doc/user/group/custom_project_templates.md b/doc/user/group/custom_project_templates.md
new file mode 100644
index 00000000000..eaf0273050b
--- /dev/null
+++ b/doc/user/group/custom_project_templates.md
@@ -0,0 +1,23 @@
+# Custom group-level project templates **[PREMIUM ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing) 11.6.
+
+When you create a new project, creating it based on custom project templates is
+a convenient option to bootstrap from an existing project boilerplate.
+The group-level setting to configure a GitLab group that serves as template
+source can be found under **Group > Settings > General > Custom project templates**.
+
+Within this section, you can configure the group where all the custom project
+templates are sourced. Every project directly under the group namespace will be
+available to the user if they have access to them. For example, every public
+project in the group will be available to every logged in user. However,
+private projects will be available only if the user has view [permissions](../permissions.md)
+in the project. That is, users with Owner, Maintainer, Developer, Reporter or Guest roles for projects,
+or for groups to which the project belongs.
+
+Projects of nested subgroups of a selected template source cannot be used.
+
+Repository and database information that are copied over to each new project are
+identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md).
+
+If you would like to set project templates at an instance level, please see [Custom instance-level project templates](../admin_area/custom_project_templates.md). \ No newline at end of file
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index d673fa4d21a..36b9318c0e0 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -259,6 +259,11 @@ types with every project in a group.
Learn more about [Group-level file templates](https://docs.gitlab.com/ee/user/group/index.html#group-level-file-templates-premium).
+#### Group-level project templates **[PREMIUM]**
+
+Define project templates at a group-level by setting a group as a template source.
+[Learn more about group-level project templates](custom_project_templates.md).
+
### Advanced settings
- **Projects**: view all projects within that group, add members to each project,
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 6c6119a2691..debebd4c081 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -1022,7 +1022,7 @@ A link starting with a `/` is relative to the wiki root.
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
-[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX"
+[katex-subset]: https://katex.org/docs/supported.html "Macros supported by KaTeX"
[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
[commonmarker]: https://github.com/gjtorikian/commonmarker
[commonmark-spec]: https://spec.commonmark.org/current/
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 2aa7c7ef815..79b36e5263e 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -92,13 +92,47 @@ To add an existing Kubernetes cluster to your project:
the `ca.crt` contents here.
- **Token** -
GitLab authenticates against Kubernetes using service tokens, which are
- scoped to a particular `namespace`. If you don't have a service token yet,
- you can follow the
- [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
- to create one. You can also view or create service tokens in the
- [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/)
- (under **Config > Secrets**). **The account that will issue the service token
- must have admin privileges on the cluster.**
+ scoped to a particular `namespace`.
+ **The token used should belong to a service account with
+ [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
+ privileges.** To create this service account:
+
+ 1. Create a `gitlab` service account in the `default` namespace:
+
+ ```bash
+ kubectl create -f - <<EOF
+ apiVersion: v1
+ kind: ServiceAccount
+ metadata:
+ name: gitlab
+ namespace: default
+ EOF
+ ```
+ 1. Create a cluster role binding to give the `gitlab` service account
+ `cluster-admin` privileges:
+
+ ```bash
+ kubectl create -f - <<EOF
+ kind: ClusterRoleBinding
+ apiVersion: rbac.authorization.k8s.io/v1
+ metadata:
+ name: gitlab-cluster-admin
+ subjects:
+ - kind: ServiceAccount
+ name: gitlab
+ namespace: default
+ roleRef:
+ kind: ClusterRole
+ name: cluster-admin
+ apiGroup: rbac.authorization.k8s.io
+ EOF
+ ```
+ NOTE: **Note:**
+ For GKE clusters, you will need the
+ `container.clusterRoleBindings.create` permission to create a cluster
+ role binding. You can follow the [Google Cloud
+ documentation](https://cloud.google.com/iam/docs/granting-changing-revoking-access)
+ to grant access.
- **Project namespace** (optional) - You don't have to fill it in; by leaving
it blank, GitLab will create one for you. Also:
- Each project should have a unique namespace.
@@ -142,8 +176,9 @@ Whether ABAC or RBAC is enabled, GitLab will create the necessary
service accounts and privileges in order to install and run
[GitLab managed applications](#installing-applications):
-- A `gitlab` service account with `cluster-admin` privileges will be created in the
- `default` namespace, which will be used by GitLab to manage the newly created cluster.
+- If GitLab is creating the cluster, a `gitlab` service account with
+ `cluster-admin` privileges will be created in the `default` namespace,
+ which will be used by GitLab to manage the newly created cluster.
- A project service account with [`edit`
privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index 9daacc37994..051277dfe02 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -83,12 +83,12 @@ The next time a pipeline is scheduled, your credentials will be used.
![Schedules list](img/pipeline_schedules_ownership.png)
-> **Note:**
-When the owner of the schedule doesn't have the ability to create pipelines
-anymore, due to e.g., being blocked or removed from the project, or lacking
-the permission to run on protected branches or tags. When this happened, the
-schedule is deactivated. Another user can take ownership and activate it, so
-the schedule can be run again.
+NOTE: **Note:**
+If the owner of a pipeline schedule doesn't have the ability to create pipelines
+on the target branch, the schedule will stop creating new pipelines. This can
+happen if, for example, the owner is blocked or removed from the project, or
+the target branch or tag is protected. In this case, someone with sufficient
+privileges must take ownership of the schedule.
## Advanced admin configuration
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index cecff6d3b81..ee8dc822098 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Gets a list of access requests for a #{source_type}." do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::AccessRequester
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 449faf5f8da..a4bf0d77eb1 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -7,8 +7,8 @@ module API
LOG_FILENAME = Rails.root.join("log", "api_json.log")
NO_SLASH_URL_PART_REGEX = %r{[^/]+}
- PROJECT_ENDPOINT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
- COMMIT_ENDPOINT_REQUIREMENTS = PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
+ NAMESPACE_OR_PROJECT_REQUIREMENTS = { id: NO_SLASH_URL_PART_REGEX }.freeze
+ COMMIT_ENDPOINT_REQUIREMENTS = NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(sha: NO_SLASH_URL_PART_REGEX).freeze
insert_before Grape::Middleware::Error,
GrapeLogging::Middleware::RequestLogger,
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 61357b3f1d6..af9b519ed9e 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -94,6 +94,7 @@ module API
Gitlab::Auth::TokenNotFoundError,
Gitlab::Auth::ExpiredError,
Gitlab::Auth::RevokedError,
+ Gitlab::Auth::ImpersonationDisabled,
Gitlab::Auth::InsufficientScopeError]
base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend
@@ -121,6 +122,11 @@ module API
:invalid_token,
"Token was revoked. You have to re-authorize from the user.")
+ when Gitlab::Auth::ImpersonationDisabled
+ Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
+ :invalid_token,
+ "Token is an impersonation token but impersonation was disabled.")
+
when Gitlab::Auth::InsufficientScopeError
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
# does not include WWW-Authenticate header, which breaks the standard.
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index c2abf9155f3..a1851ba3627 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -14,7 +14,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
AWARDABLES.each do |awardable_params|
awardable_string = awardable_params[:type].pluralize
awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}"
diff --git a/lib/api/badges.rb b/lib/api/badges.rb
index ab670988f47..ba554e00a16 100644
--- a/lib/api/badges.rb
+++ b/lib/api/badges.rb
@@ -22,7 +22,7 @@ module API
params do
requires :id, type: String, desc: "The ID of a #{source_type}"
end
- resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Gets a list of #{source_type} badges viewable by the authenticated user." do
detail 'This feature was introduced in GitLab 10.6.'
success Entities::Badge
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index c80e1c57864..b7c77730afb 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -16,7 +16,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
segment ':id/boards' do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 2735d410c8e..e7e58ad0e66 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -6,7 +6,7 @@ module API
class Branches < Grape::API
include PaginationParams
- BRANCH_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX)
+ BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX)
before { authorize! :download_code, user_project }
@@ -20,7 +20,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a project repository branches' do
success Entities::Branch
end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 99553d993ca..1ba2c150cb4 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
include PaginationParams
before { authenticate! }
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 337b92a6183..9d23daafe95 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -23,7 +23,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a project repository commits' do
success Entities::Commit
end
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index ce35720d408..df6d2721977 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -31,7 +31,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of the project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before { authorize_admin_project }
desc "Get a specific project's deploy keys" do
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index 6747e2e5005..8706a971a1a 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all deployments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Deployment
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 39c6d28391d..91eb6a23701 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -17,7 +17,7 @@ module API
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
end
- resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a list of #{noteable_type.to_s.downcase} discussions" do
success Entities::Discussion
end
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index c64217a6977..633f24d3c9a 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -11,7 +11,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all environments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Environment
diff --git a/lib/api/events.rb b/lib/api/events.rb
index 6e0b508be19..44dae57770d 100644
--- a/lib/api/events.rb
+++ b/lib/api/events.rb
@@ -97,7 +97,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "List a Project's visible events" do
success Entities::Event
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index bcd2cd48a45..becf66d1467 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -2,7 +2,7 @@
module API
class Files < Grape::API
- FILE_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX)
+ FILE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX)
# Prevents returning plain/text responses for files with .txt extension
after_validation { content_type "application/json" }
diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb
index dc30e868e2e..9a20ee8c8b9 100644
--- a/lib/api/group_boards.rb
+++ b/lib/api/group_boards.rb
@@ -19,7 +19,7 @@ module API
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
segment ':id/boards' do
desc 'Find a group board' do
detail 'This feature was introduced in 10.6'
diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb
index b36436dbf43..d4287e4a7c4 100644
--- a/lib/api/group_milestones.rb
+++ b/lib/api/group_milestones.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group milestones' do
success Entities::Milestone
end
diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb
index ae7241e9a30..3f048e0dc56 100644
--- a/lib/api/group_variables.rb
+++ b/lib/api/group_variables.rb
@@ -11,7 +11,7 @@ module API
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get group-level variables' do
success Entities::Variable
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index b3d10721692..626a2008dee 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -140,7 +140,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Update a group. Available only for users who can administrate groups.' do
success Entities::Group
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 491b5085bb8..dac700482b4 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -101,7 +101,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group issues' do
success Entities::IssueBasic
end
@@ -128,7 +128,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb
index 2229cbcd9d4..7c2d8ff11bf 100644
--- a/lib/api/job_artifacts.rb
+++ b/lib/api/job_artifacts.rb
@@ -14,7 +14,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Download the artifacts archive from a job' do
detail 'This feature was introduced in GitLab 8.10'
end
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 697555c9605..c3f8a84d742 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
params :optional_scope do
optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 28555454307..2e676b0aa6b 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all labels of the project' do
success Entities::Label
end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index a8f67be3463..461ffe71a62 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Gets a list of group or project members viewable by the authenticated user.' do
success Entities::Member
end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index e4fb890960a..6ad30aa56e0 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of merge request diff versions' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::MergeRequestDiff
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 595b3641c52..8c1951cc535 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -135,7 +135,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group merge requests' do
success Entities::MergeRequestBasic
end
@@ -154,7 +154,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
include TimeTrackingEndpoints
helpers do
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index 76639fbb031..06a57e3cd6f 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -28,7 +28,7 @@ module API
params do
requires :id, type: String, desc: "Namespace's ID or path"
end
- get ':id' do
+ get ':id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
present user_namespace, with: Entities::Namespace, current_user: current_user
end
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 9f323b87baf..1bdf7aeb119 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -16,7 +16,7 @@ module API
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
end
- resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
noteables_str = noteable_type.to_s.underscore.pluralize
desc "Get a list of #{noteable_type.to_s.downcase} notes" do
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index 4d9a4629268..8cb46bd3ad6 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -58,7 +58,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get #{source_type} level notification level settings, defaults to Global" do
detail 'This feature was introduced in GitLab 8.12'
success Entities::NotificationSetting
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index c9ad47e0f0d..78442f465bd 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -4,7 +4,7 @@ module API
class PagesDomains < Grape::API
include PaginationParams
- PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX)
+ PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX)
before do
authenticate!
@@ -54,7 +54,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before do
require_pages_enabled!
end
diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb
index ed0a38b9d70..47b711917e2 100644
--- a/lib/api/pipeline_schedules.rb
+++ b/lib/api/pipeline_schedules.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all pipeline schedules' do
success Entities::PipelineSchedule
end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index cba1e3a6684..62d07d945e2 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::PipelineBasic
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 4af4c6ac593..0e7576c9243 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -29,7 +29,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get project hooks' do
success Entities::ProjectHook
end
diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb
index cbfa0c5bc1c..c64ec2fcc95 100644
--- a/lib/api/project_import.rb
+++ b/lib/api/project_import.rb
@@ -23,7 +23,7 @@ module API
forbidden! unless Gitlab::CurrentSettings.import_sources.include?('gitlab_project')
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
requires :path, type: String, desc: 'The new project path and name'
requires :file, type: File, desc: 'The project export file to be imported'
diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb
index c7137ba5217..da31bcb8dac 100644
--- a/lib/api/project_milestones.rb
+++ b/lib/api/project_milestones.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of project milestones' do
success Entities::Milestone
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index f3a1b73b153..a607df411a6 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 0a914f9012e..f5d21d8923f 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -128,7 +128,7 @@ module API
end
end
- resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :users, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a user projects' do
success Entities::BasicProjectDetails
end
@@ -224,7 +224,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a single project' do
success Entities::ProjectWithAccess
end
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index 47752f40e58..5af43448727 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -4,14 +4,14 @@ module API
class ProtectedBranches < Grape::API
include PaginationParams
- BRANCH_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
+ BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
before { authorize_admin_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a project's protected branches" do
success Entities::ProtectedBranch
end
diff --git a/lib/api/protected_tags.rb b/lib/api/protected_tags.rb
index ed1c5f0cc05..ee13473c848 100644
--- a/lib/api/protected_tags.rb
+++ b/lib/api/protected_tags.rb
@@ -4,14 +4,14 @@ module API
class ProtectedTags < Grape::API
include PaginationParams
- TAG_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
+ TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
before { authorize_admin_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a project's protected tags" do
detail 'This feature was introduced in GitLab 11.3.'
success Entities::ProtectedTag
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index dc844c0bd27..32e05d84491 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -11,7 +11,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb
index b6fbe8c0235..0c328f7268e 100644
--- a/lib/api/resource_label_events.rb
+++ b/lib/api/resource_label_events.rb
@@ -16,7 +16,7 @@ module API
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
end
- resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a list of #{eventable_type.to_s.downcase} resource label events" do
success Entities::ResourceLabelEvent
detail 'This feature was introduced in 11.3'
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index ce70460af11..f72b33605a7 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -126,7 +126,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before { authorize_admin_project }
desc 'Get runners available for project' do
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 12d97dcfe7f..5900e1cccc2 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -70,7 +70,7 @@ module API
end
end
- resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Search on GitLab' do
detail 'This feature was introduced in GitLab 10.5.'
end
@@ -89,7 +89,7 @@ module API
end
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Search on GitLab' do
detail 'This feature was introduced in GitLab 10.5.'
end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 1cb3b8a7277..d60f0f5f08d 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -763,7 +763,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before { authenticate! }
before { authorize_admin_project }
@@ -842,7 +842,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Trigger a slash command for #{service_slug}" do
detail 'Added in GitLab 8.13'
end
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index 077e9373ac4..74ad3c35a61 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -14,7 +14,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
requires :subscribable_id, type: String, desc: 'The ID of a resource'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
entity_class = Entities.const_get(type_singularized.camelcase)
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index f739eacf9ba..b18eec7d796 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -4,14 +4,14 @@ module API
class Tags < Grape::API
include PaginationParams
- TAG_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX)
+ TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX)
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a project repository tags' do
success Entities::Tag
end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index ed2cf2cc31b..d2c8cf7c1aa 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -14,7 +14,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
ISSUABLE_TYPES.each do |type, finder|
type_id_str = "#{type.singularize}_iid".to_sym
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index f784c857883..3ce1529f259 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Trigger a GitLab project pipeline' do
success Entities::Pipeline
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index c844ba321ed..f7cae2251c2 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -11,7 +11,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get project variables' do
success Entities::Variable
end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index 24746f4efc6..302b2797a34 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -22,7 +22,7 @@ module API
end
end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of wiki pages' do
success Entities::WikiPageBasic
end
@@ -103,7 +103,7 @@ module API
requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded'
optional :branch, type: String, desc: 'The name of the branch'
end
- post ":id/wikis/attachments", requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ post ":id/wikis/attachments", requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
authorize! :create_wiki, user_project
result = ::Wikis::CreateAttachmentService.new(user_project,
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index adba9084845..a5efe33bdc6 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -7,6 +7,7 @@ module Gitlab
TokenNotFoundError = Class.new(AuthenticationError)
ExpiredError = Class.new(AuthenticationError)
RevokedError = Class.new(AuthenticationError)
+ ImpersonationDisabled = Class.new(AuthenticationError)
UnauthorizedError = Class.new(AuthenticationError)
class InsufficientScopeError < AuthenticationError
@@ -67,6 +68,8 @@ module Gitlab
raise ExpiredError
when AccessTokenValidationService::REVOKED
raise RevokedError
+ when AccessTokenValidationService::IMPERSONATION_DISABLED
+ raise ImpersonationDisabled
end
end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index 2fb3c4582e7..6333799a491 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -15,7 +15,7 @@ module Gitlab
@global = Entry::Global.new(@config)
@global.compose!
- rescue Loader::FormatError,
+ rescue Gitlab::Config::Loader::FormatError,
Extendable::ExtensionError,
External::Processor::IncludeError => e
raise Config::ConfigError, e.message
@@ -71,7 +71,7 @@ module Gitlab
private
def build_config(config, opts = {})
- initial_config = Loader.new(config).load!
+ initial_config = Gitlab::Config::Loader::Yaml.new(config).load!
project = opts.fetch(:project, nil)
if project
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index ef5f25b42c0..41613369ca2 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -7,10 +7,10 @@ module Gitlab
##
# Entry that represents a configuration of job artifacts.
#
- class Artifacts < Node
- include Configurable
- include Validatable
- include Attributable
+ class Artifacts < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[name untracked paths reports when expire_in].freeze
diff --git a/lib/gitlab/ci/config/entry/attributable.rb b/lib/gitlab/ci/config/entry/attributable.rb
deleted file mode 100644
index 3c2e1df9b83..00000000000
--- a/lib/gitlab/ci/config/entry/attributable.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- module Attributable
- extend ActiveSupport::Concern
-
- class_methods do
- def attributes(*attributes)
- attributes.flatten.each do |attribute|
- if method_defined?(attribute)
- raise ArgumentError, 'Method already defined!'
- end
-
- define_method(attribute) do
- return unless config.is_a?(Hash)
-
- config[attribute]
- end
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/boolean.rb b/lib/gitlab/ci/config/entry/boolean.rb
deleted file mode 100644
index b9639c83075..00000000000
--- a/lib/gitlab/ci/config/entry/boolean.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # Entry that represents a boolean value.
- #
- class Boolean < Node
- include Validatable
-
- validations do
- validates :config, boolean: true
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/cache.rb b/lib/gitlab/ci/config/entry/cache.rb
index 0a25057f482..7b94af24c09 100644
--- a/lib/gitlab/ci/config/entry/cache.rb
+++ b/lib/gitlab/ci/config/entry/cache.rb
@@ -7,9 +7,9 @@ module Gitlab
##
# Entry that represents a cache configuration
#
- class Cache < Node
- include Configurable
- include Attributable
+ class Cache < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[key untracked paths policy].freeze
DEFAULT_POLICY = 'pull-push'.freeze
@@ -22,7 +22,7 @@ module Gitlab
entry :key, Entry::Key,
description: 'Cache key used to define a cache affinity.'
- entry :untracked, Entry::Boolean,
+ entry :untracked, ::Gitlab::Config::Entry::Boolean,
description: 'Cache all untracked files.'
entry :paths, Entry::Paths,
diff --git a/lib/gitlab/ci/config/entry/commands.rb b/lib/gitlab/ci/config/entry/commands.rb
index d9658291ebe..02e368c1813 100644
--- a/lib/gitlab/ci/config/entry/commands.rb
+++ b/lib/gitlab/ci/config/entry/commands.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a job script.
#
- class Commands < Node
- include Validatable
+ class Commands < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings_or_string: true
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
deleted file mode 100644
index 4aabf0cfa31..00000000000
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ /dev/null
@@ -1,83 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # This mixin is responsible for adding DSL, which purpose is to
- # simplifly process of adding child nodes.
- #
- # This can be used only if parent node is a configuration entry that
- # holds a hash as a configuration value, for example:
- #
- # job:
- # script: ...
- # artifacts: ...
- #
- module Configurable
- extend ActiveSupport::Concern
-
- included do
- include Validatable
-
- validations do
- validates :config, type: Hash
- end
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def compose!(deps = nil)
- return unless valid?
-
- self.class.nodes.each do |key, factory|
- factory
- .value(config[key])
- .with(key: key, parent: self)
-
- entries[key] = factory.create!
- end
-
- yield if block_given?
-
- entries.each_value do |entry|
- entry.compose!(deps)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- class_methods do
- def nodes
- Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
- end
-
- private
-
- # rubocop: disable CodeReuse/ActiveRecord
- def entry(key, entry, metadata)
- factory = Entry::Factory.new(entry)
- .with(description: metadata[:description])
-
- (@nodes ||= {}).merge!(key.to_sym => factory)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def helpers(*nodes)
- nodes.each do |symbol|
- define_method("#{symbol}_defined?") do
- entries[symbol]&.specified?
- end
-
- define_method("#{symbol}_value") do
- return unless entries[symbol] && entries[symbol].valid?
-
- entries[symbol].value
- end
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb
index 690409ccf77..89545158bed 100644
--- a/lib/gitlab/ci/config/entry/coverage.rb
+++ b/lib/gitlab/ci/config/entry/coverage.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents Coverage settings.
#
- class Coverage < Node
- include Validatable
+ class Coverage < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, regexp: true
diff --git a/lib/gitlab/ci/config/entry/environment.rb b/lib/gitlab/ci/config/entry/environment.rb
index 07e9e1d3f67..69a3a1aedef 100644
--- a/lib/gitlab/ci/config/entry/environment.rb
+++ b/lib/gitlab/ci/config/entry/environment.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents an environment.
#
- class Environment < Node
- include Validatable
+ class Environment < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
ALLOWED_KEYS = %i[name url action on_stop].freeze
diff --git a/lib/gitlab/ci/config/entry/factory.rb b/lib/gitlab/ci/config/entry/factory.rb
deleted file mode 100644
index 85c9c3511a4..00000000000
--- a/lib/gitlab/ci/config/entry/factory.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # Factory class responsible for fabricating entry objects.
- #
- class Factory
- InvalidFactory = Class.new(StandardError)
-
- def initialize(entry)
- @entry = entry
- @metadata = {}
- @attributes = {}
- end
-
- def value(value)
- @value = value
- self
- end
-
- def metadata(metadata)
- @metadata.merge!(metadata)
- self
- end
-
- def with(attributes)
- @attributes.merge!(attributes)
- self
- end
-
- def create!
- raise InvalidFactory unless defined?(@value)
-
- ##
- # We assume that unspecified entry is undefined.
- # See issue #18775.
- #
- if @value.nil?
- Entry::Unspecified.new(
- fabricate_unspecified
- )
- else
- fabricate(@entry, @value)
- end
- end
-
- private
-
- def fabricate_unspecified
- ##
- # If entry has a default value we fabricate concrete node
- # with default value.
- #
- if @entry.default.nil?
- fabricate(Entry::Undefined)
- else
- fabricate(@entry, @entry.default)
- end
- end
-
- def fabricate(entry, value = nil)
- entry.new(value, @metadata).tap do |node|
- node.key = @attributes[:key]
- node.parent = @attributes[:parent]
- node.description = @attributes[:description]
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb
index eba203d9d06..09ecb5fdb99 100644
--- a/lib/gitlab/ci/config/entry/global.rb
+++ b/lib/gitlab/ci/config/entry/global.rb
@@ -8,8 +8,8 @@ module Gitlab
# This class represents a global entry - root Entry for entire
# GitLab CI Configuration file.
#
- class Global < Node
- include Configurable
+ class Global < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
entry :before_script, Entry::Script,
description: 'Script that will be executed before each job.'
@@ -49,7 +49,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def compose_jobs!
- factory = Entry::Factory.new(Entry::Jobs)
+ factory = ::Gitlab::Config::Entry::Factory.new(Entry::Jobs)
.value(@config.except(*self.class.nodes.keys))
.with(key: :jobs, parent: self,
description: 'Jobs definition for this pipeline')
diff --git a/lib/gitlab/ci/config/entry/hidden.rb b/lib/gitlab/ci/config/entry/hidden.rb
index dc0ede2a25f..76e5d05639f 100644
--- a/lib/gitlab/ci/config/entry/hidden.rb
+++ b/lib/gitlab/ci/config/entry/hidden.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a hidden CI/CD key.
#
- class Hidden < Node
- include Validatable
+ class Hidden < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, presence: true
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index fc453b72fa5..a13a0625e90 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a Docker image.
#
- class Image < Node
- include Validatable
+ class Image < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
ALLOWED_KEYS = %i[name entrypoint].freeze
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index c8cb3248fa7..4f2ac94b6c3 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -7,9 +7,9 @@ module Gitlab
##
# Entry that represents a concrete CI/CD job.
#
- class Job < Node
- include Configurable
- include Attributable
+ class Job < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Configurable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[tags script only except type image services
allow_failure type stage when start_in artifacts cache
diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb
index 1535b108000..82b72e40404 100644
--- a/lib/gitlab/ci/config/entry/jobs.rb
+++ b/lib/gitlab/ci/config/entry/jobs.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a set of jobs.
#
- class Jobs < Node
- include Validatable
+ class Jobs < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, type: Hash
@@ -34,7 +34,7 @@ module Gitlab
@config.each do |name, config|
node = hidden?(name) ? Entry::Hidden : Entry::Job
- factory = Entry::Factory.new(node)
+ factory = ::Gitlab::Config::Entry::Factory.new(node)
.value(config || {})
.metadata(name: name)
.with(key: name, parent: self,
diff --git a/lib/gitlab/ci/config/entry/key.rb b/lib/gitlab/ci/config/entry/key.rb
index 963b200c7bb..0c10967e629 100644
--- a/lib/gitlab/ci/config/entry/key.rb
+++ b/lib/gitlab/ci/config/entry/key.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a key.
#
- class Key < Node
- include Validatable
+ class Key < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, key: true
diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
deleted file mode 100644
index 4043629dea9..00000000000
--- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- module LegacyValidationHelpers
- private
-
- def validate_duration(value)
- value.is_a?(String) && ChronicDuration.parse(value)
- rescue ChronicDuration::DurationParseError
- false
- end
-
- def validate_duration_limit(value, limit)
- return false unless value.is_a?(String)
-
- ChronicDuration.parse(value).second.from_now <
- ChronicDuration.parse(limit).second.from_now
- rescue ChronicDuration::DurationParseError
- false
- end
-
- def validate_array_of_strings(values)
- values.is_a?(Array) && values.all? { |value| validate_string(value) }
- end
-
- def validate_array_of_strings_or_regexps(values)
- values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) }
- end
-
- def validate_variables(variables)
- variables.is_a?(Hash) &&
- variables.flatten.all? do |value|
- validate_string(value) || validate_integer(value)
- end
- end
-
- def validate_integer(value)
- value.is_a?(Integer)
- end
-
- def validate_string(value)
- value.is_a?(String) || value.is_a?(Symbol)
- end
-
- def validate_regexp(value)
- !value.nil? && Regexp.new(value.to_s) && true
- rescue RegexpError, TypeError
- false
- end
-
- def validate_string_or_regexp(value)
- return true if value.is_a?(Symbol)
- return false unless value.is_a?(String)
-
- if value.first == '/' && value.last == '/'
- validate_regexp(value[1...-1])
- else
- true
- end
- end
-
- def validate_boolean(value)
- value.in?([true, false])
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb
deleted file mode 100644
index 347089722e4..00000000000
--- a/lib/gitlab/ci/config/entry/node.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # Base abstract class for each configuration entry node.
- #
- class Node
- InvalidError = Class.new(StandardError)
-
- attr_reader :config, :metadata
- attr_accessor :key, :parent, :description
-
- def initialize(config, **metadata)
- @config = config
- @metadata = metadata
- @entries = {}
-
- self.class.aspects.to_a.each do |aspect|
- instance_exec(&aspect)
- end
- end
-
- def [](key)
- @entries[key] || Entry::Undefined.new
- end
-
- def compose!(deps = nil)
- return unless valid?
-
- yield if block_given?
- end
-
- def leaf?
- @entries.none?
- end
-
- def descendants
- @entries.values
- end
-
- def ancestors
- @parent ? @parent.ancestors + [@parent] : []
- end
-
- def valid?
- errors.none?
- end
-
- def errors
- []
- end
-
- def value
- if leaf?
- @config
- else
- meaningful = @entries.select do |_key, value|
- value.specified? && value.relevant?
- end
-
- Hash[meaningful.map { |key, entry| [key, entry.value] }]
- end
- end
-
- def specified?
- true
- end
-
- def relevant?
- true
- end
-
- def location
- name = @key.presence || self.class.name.to_s.demodulize
- .underscore.humanize.downcase
-
- ancestors.map(&:key).append(name).compact.join(':')
- end
-
- def inspect
- val = leaf? ? config : descendants
- unspecified = specified? ? '' : '(unspecified) '
- "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>"
- end
-
- def self.default
- end
-
- def self.aspects
- @aspects ||= []
- end
-
- private
-
- attr_reader :entries
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/paths.rb b/lib/gitlab/ci/config/entry/paths.rb
index 9580b5e2e7f..d6f287c6552 100644
--- a/lib/gitlab/ci/config/entry/paths.rb
+++ b/lib/gitlab/ci/config/entry/paths.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents an array of paths.
#
- class Paths < Node
- include Validatable
+ class Paths < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings: true
diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb
index 0535d7c1a1a..998da1f6837 100644
--- a/lib/gitlab/ci/config/entry/policy.rb
+++ b/lib/gitlab/ci/config/entry/policy.rb
@@ -7,12 +7,12 @@ module Gitlab
##
# Entry that represents an only/except trigger policy for the job.
#
- class Policy < Simplifiable
+ class Policy < ::Gitlab::Config::Entry::Simplifiable
strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) }
strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) }
- class RefsPolicy < Entry::Node
- include Entry::Validatable
+ class RefsPolicy < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings_or_regexps: true
@@ -23,9 +23,9 @@ module Gitlab
end
end
- class ComplexPolicy < Entry::Node
- include Entry::Validatable
- include Entry::Attributable
+ class ComplexPolicy < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[refs kubernetes variables changes].freeze
attributes :refs, :kubernetes, :variables, :changes
@@ -58,7 +58,7 @@ module Gitlab
end
end
- class UnknownStrategy < Entry::Node
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
def errors
["#{location} has to be either an array of conditions or a hash"]
end
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 3ac2a6fa777..a3f6cc31321 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -7,9 +7,9 @@ module Gitlab
##
# Entry that represents a configuration of job artifacts.
#
- class Reports < Node
- include Validatable
- include Attributable
+ class Reports < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management].freeze
diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb
index ee82ab10f9c..eaf8b38aa3c 100644
--- a/lib/gitlab/ci/config/entry/retry.rb
+++ b/lib/gitlab/ci/config/entry/retry.rb
@@ -7,12 +7,12 @@ module Gitlab
##
# Entry that represents a retry config for a job.
#
- class Retry < Simplifiable
+ class Retry < ::Gitlab::Config::Entry::Simplifiable
strategy :SimpleRetry, if: -> (config) { config.is_a?(Integer) }
strategy :FullRetry, if: -> (config) { config.is_a?(Hash) }
- class SimpleRetry < Entry::Node
- include Entry::Validatable
+ class SimpleRetry < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, numericality: { only_integer: true,
@@ -31,9 +31,9 @@ module Gitlab
end
end
- class FullRetry < Entry::Node
- include Entry::Validatable
- include Entry::Attributable
+ class FullRetry < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[max when].freeze
attributes :max, :when
@@ -73,7 +73,7 @@ module Gitlab
end
end
- class UnknownStrategy < Entry::Node
+ class UnknownStrategy < ::Gitlab::Config::Entry::Node
def errors
["#{location} has to be either an integer or a hash"]
end
diff --git a/lib/gitlab/ci/config/entry/script.rb b/lib/gitlab/ci/config/entry/script.rb
index f7d39e5cf55..9d25a82b521 100644
--- a/lib/gitlab/ci/config/entry/script.rb
+++ b/lib/gitlab/ci/config/entry/script.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a script.
#
- class Script < Node
- include Validatable
+ class Script < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings: true
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index 47bf9205147..6df67083310 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -8,7 +8,7 @@ module Gitlab
# Entry that represents a configuration of Docker service.
#
class Service < Image
- include Validatable
+ include ::Gitlab::Config::Entry::Validatable
ALLOWED_KEYS = %i[name entrypoint command alias].freeze
diff --git a/lib/gitlab/ci/config/entry/services.rb b/lib/gitlab/ci/config/entry/services.rb
index bdf7f80f382..71475f69218 100644
--- a/lib/gitlab/ci/config/entry/services.rb
+++ b/lib/gitlab/ci/config/entry/services.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a configuration of Docker services.
#
- class Services < Node
- include Validatable
+ class Services < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, type: Array
@@ -18,7 +18,7 @@ module Gitlab
super do
@entries = []
@config.each do |config|
- @entries << Entry::Factory.new(Entry::Service)
+ @entries << ::Gitlab::Config::Entry::Factory.new(Entry::Service)
.value(config || {})
.create!
end
diff --git a/lib/gitlab/ci/config/entry/simplifiable.rb b/lib/gitlab/ci/config/entry/simplifiable.rb
deleted file mode 100644
index 9961bbfaa40..00000000000
--- a/lib/gitlab/ci/config/entry/simplifiable.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- class Simplifiable < SimpleDelegator
- EntryStrategy = Struct.new(:name, :condition)
-
- def initialize(config, **metadata)
- unless self.class.const_defined?(:UnknownStrategy)
- raise ArgumentError, 'UndefinedStrategy not available!'
- end
-
- strategy = self.class.strategies.find do |variant|
- variant.condition.call(config)
- end
-
- entry = self.class.entry_class(strategy)
-
- super(entry.new(config, metadata))
- end
-
- def self.strategy(name, **opts)
- EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy|
- strategies.append(strategy)
- end
- end
-
- def self.strategies
- @strategies ||= []
- end
-
- def self.entry_class(strategy)
- if strategy.present?
- self.const_get(strategy.name)
- else
- self::UnknownStrategy
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/stage.rb b/lib/gitlab/ci/config/entry/stage.rb
index 65ab5953131..d6d576a3139 100644
--- a/lib/gitlab/ci/config/entry/stage.rb
+++ b/lib/gitlab/ci/config/entry/stage.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a stage for a job.
#
- class Stage < Node
- include Validatable
+ class Stage < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, type: String
diff --git a/lib/gitlab/ci/config/entry/stages.rb b/lib/gitlab/ci/config/entry/stages.rb
index ab184246d29..2d715cbc6bb 100644
--- a/lib/gitlab/ci/config/entry/stages.rb
+++ b/lib/gitlab/ci/config/entry/stages.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents a configuration for pipeline stages.
#
- class Stages < Node
- include Validatable
+ class Stages < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings: true
diff --git a/lib/gitlab/ci/config/entry/undefined.rb b/lib/gitlab/ci/config/entry/undefined.rb
deleted file mode 100644
index 77dcfa88170..00000000000
--- a/lib/gitlab/ci/config/entry/undefined.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # This class represents an undefined entry.
- #
- class Undefined < Node
- def initialize(*)
- super(nil)
- end
-
- def value
- nil
- end
-
- def valid?
- true
- end
-
- def errors
- []
- end
-
- def specified?
- false
- end
-
- def relevant?
- false
- end
-
- def inspect
- "#<#{self.class.name}>"
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/unspecified.rb b/lib/gitlab/ci/config/entry/unspecified.rb
deleted file mode 100644
index bab32489d2f..00000000000
--- a/lib/gitlab/ci/config/entry/unspecified.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # This class represents an unspecified entry.
- #
- # It decorates original entry adding method that indicates it is
- # unspecified.
- #
- class Unspecified < SimpleDelegator
- def specified?
- false
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb
deleted file mode 100644
index 08a6593c980..00000000000
--- a/lib/gitlab/ci/config/entry/validatable.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- module Validatable
- extend ActiveSupport::Concern
-
- def self.included(node)
- node.aspects.append -> do
- @validator = self.class.validator.new(self)
- @validator.validate(:new)
- end
- end
-
- def errors
- @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- end
-
- class_methods do
- def validator
- @validator ||= Class.new(Entry::Validator).tap do |validator|
- if defined?(@validations)
- @validations.each { |rules| validator.class_eval(&rules) }
- end
- end
- end
-
- private
-
- def validations(&block)
- (@validations ||= []).append(block)
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/validator.rb b/lib/gitlab/ci/config/entry/validator.rb
deleted file mode 100644
index 33ffdd3a95d..00000000000
--- a/lib/gitlab/ci/config/entry/validator.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- class Validator < SimpleDelegator
- include ActiveModel::Validations
- include Entry::Validators
-
- def initialize(entry)
- super(entry)
- end
-
- def messages
- errors.full_messages.map do |error|
- "#{location} #{error}".downcase
- end
- end
-
- def self.name
- 'Validator'
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
deleted file mode 100644
index a1d552fb2e5..00000000000
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ /dev/null
@@ -1,198 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- class Config
- module Entry
- module Validators
- class AllowedKeysValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unknown_keys = value.try(:keys).to_a - options[:in]
-
- if unknown_keys.any?
- record.errors.add(attribute, "contains unknown keys: " +
- unknown_keys.join(', '))
- end
- end
- end
-
- class AllowedValuesValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unless options[:in].include?(value.to_s)
- record.errors.add(attribute, "unknown value: #{value}")
- end
- end
- end
-
- class AllowedArrayValuesValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unkown_values = value - options[:in]
- unless unkown_values.empty?
- record.errors.add(attribute, "contains unknown values: " +
- unkown_values.join(', '))
- end
- end
- end
-
- class ArrayOfStringsValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_array_of_strings(value)
- record.errors.add(attribute, 'should be an array of strings')
- end
- end
- end
-
- class BooleanValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_boolean(value)
- record.errors.add(attribute, 'should be a boolean value')
- end
- end
- end
-
- class DurationValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_duration(value)
- record.errors.add(attribute, 'should be a duration')
- end
-
- if options[:limit]
- unless validate_duration_limit(value, options[:limit])
- record.errors.add(attribute, 'should not exceed the limit')
- end
- end
- end
- end
-
- class HashOrStringValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unless value.is_a?(Hash) || value.is_a?(String)
- record.errors.add(attribute, 'should be a hash or a string')
- end
- end
- end
-
- class HashOrIntegerValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unless value.is_a?(Hash) || value.is_a?(Integer)
- record.errors.add(attribute, 'should be a hash or an integer')
- end
- end
- end
-
- class KeyValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- if validate_string(value)
- validate_path(record, attribute, value)
- else
- record.errors.add(attribute, 'should be a string or symbol')
- end
- end
-
- private
-
- def validate_path(record, attribute, value)
- path = CGI.unescape(value.to_s)
-
- if path.include?('/')
- record.errors.add(attribute, 'cannot contain the "/" character')
- elsif path == '.' || path == '..'
- record.errors.add(attribute, 'cannot be "." or ".."')
- end
- end
- end
-
- class RegexpValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_regexp(value)
- record.errors.add(attribute, 'must be a regular expression')
- end
- end
-
- private
-
- def look_like_regexp?(value)
- value.is_a?(String) && value.start_with?('/') &&
- value.end_with?('/')
- end
-
- def validate_regexp(value)
- look_like_regexp?(value) &&
- Regexp.new(value.to_s[1...-1]) &&
- true
- rescue RegexpError
- false
- end
- end
-
- class ArrayOfStringsOrRegexpsValidator < RegexpValidator
- def validate_each(record, attribute, value)
- unless validate_array_of_strings_or_regexps(value)
- record.errors.add(attribute, 'should be an array of strings or regexps')
- end
- end
-
- private
-
- def validate_array_of_strings_or_regexps(values)
- values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp))
- end
-
- def validate_string_or_regexp(value)
- return false unless value.is_a?(String)
- return validate_regexp(value) if look_like_regexp?(value)
-
- true
- end
- end
-
- class ArrayOfStringsOrStringValidator < RegexpValidator
- def validate_each(record, attribute, value)
- unless validate_array_of_strings_or_string(value)
- record.errors.add(attribute, 'should be an array of strings or a string')
- end
- end
-
- private
-
- def validate_array_of_strings_or_string(values)
- validate_array_of_strings(values) || validate_string(values)
- end
- end
-
- class TypeValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- type = options[:with]
- raise unless type.is_a?(Class)
-
- unless value.is_a?(type)
- message = options[:message] || "should be a #{type.name}"
- record.errors.add(attribute, message)
- end
- end
- end
-
- class VariablesValidator < ActiveModel::EachValidator
- include LegacyValidationHelpers
-
- def validate_each(record, attribute, value)
- unless validate_variables(value)
- record.errors.add(attribute, 'should be a hash of key value pairs')
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb
index 6fd3cec2f5f..89d790ebfa6 100644
--- a/lib/gitlab/ci/config/entry/variables.rb
+++ b/lib/gitlab/ci/config/entry/variables.rb
@@ -7,8 +7,8 @@ module Gitlab
##
# Entry that represents environment variables.
#
- class Variables < Node
- include Validatable
+ class Variables < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, variables: true
diff --git a/lib/gitlab/ci/config/external/file/base.rb b/lib/gitlab/ci/config/external/file/base.rb
index 15ca47ef60e..ee4ea9bbb1d 100644
--- a/lib/gitlab/ci/config/external/file/base.rb
+++ b/lib/gitlab/ci/config/external/file/base.rb
@@ -37,8 +37,8 @@ module Gitlab
end
def to_hash
- @hash ||= Ci::Config::Loader.new(content).load!
- rescue Ci::Config::Loader::FormatError
+ @hash ||= Gitlab::Config::Loader::Yaml.new(content).load!
+ rescue Gitlab::Config::Loader::FormatError
nil
end
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index c90976b2040..49f03d2ae19 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -658,6 +658,7 @@ rollout 100%:
fi
if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then
+ echo "Deploying first release with database initialization..."
helm upgrade --install \
--wait \
--set service.enabled="$service_enabled" \
@@ -680,6 +681,7 @@ rollout 100%:
"$name" \
chart/
+ echo "Deploying second release..."
helm upgrade --reuse-values \
--wait \
--set application.initializeCommand="" \
@@ -688,6 +690,7 @@ rollout 100%:
"$name" \
chart/
else
+ echo "Deploying new release..."
helm upgrade --install \
--wait \
--set service.enabled="$service_enabled" \
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index e6ec400e476..172926b8ab0 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -5,7 +5,7 @@ module Gitlab
class YamlProcessor
ValidationError = Class.new(StandardError)
- include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
+ include Gitlab::Config::Entry::LegacyValidationHelpers
attr_reader :cache, :stages, :jobs
diff --git a/lib/gitlab/config/entry/attributable.rb b/lib/gitlab/config/entry/attributable.rb
new file mode 100644
index 00000000000..560fe63df0e
--- /dev/null
+++ b/lib/gitlab/config/entry/attributable.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ module Attributable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def attributes(*attributes)
+ attributes.flatten.each do |attribute|
+ if method_defined?(attribute)
+ raise ArgumentError, 'Method already defined!'
+ end
+
+ define_method(attribute) do
+ return unless config.is_a?(Hash)
+
+ config[attribute]
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/boolean.rb b/lib/gitlab/config/entry/boolean.rb
new file mode 100644
index 00000000000..1e8a57356e3
--- /dev/null
+++ b/lib/gitlab/config/entry/boolean.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # Entry that represents a boolean value.
+ #
+ class Boolean < Node
+ include Validatable
+
+ validations do
+ validates :config, boolean: true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb
new file mode 100644
index 00000000000..afdb60b2cd5
--- /dev/null
+++ b/lib/gitlab/config/entry/configurable.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # This mixin is responsible for adding DSL, which purpose is to
+ # simplifly process of adding child nodes.
+ #
+ # This can be used only if parent node is a configuration entry that
+ # holds a hash as a configuration value, for example:
+ #
+ # job:
+ # script: ...
+ # artifacts: ...
+ #
+ module Configurable
+ extend ActiveSupport::Concern
+
+ included do
+ include Validatable
+
+ validations do
+ validates :config, type: Hash
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def compose!(deps = nil)
+ return unless valid?
+
+ self.class.nodes.each do |key, factory|
+ factory
+ .value(config[key])
+ .with(key: key, parent: self)
+
+ entries[key] = factory.create!
+ end
+
+ yield if block_given?
+
+ entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ class_methods do
+ def nodes
+ Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def entry(key, entry, metadata)
+ factory = ::Gitlab::Config::Entry::Factory.new(entry)
+ .with(description: metadata[:description])
+
+ (@nodes ||= {}).merge!(key.to_sym => factory)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def helpers(*nodes)
+ nodes.each do |symbol|
+ define_method("#{symbol}_defined?") do
+ entries[symbol]&.specified?
+ end
+
+ define_method("#{symbol}_value") do
+ return unless entries[symbol] && entries[symbol].valid?
+
+ entries[symbol].value
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb
new file mode 100644
index 00000000000..30d43c9f9a1
--- /dev/null
+++ b/lib/gitlab/config/entry/factory.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # Factory class responsible for fabricating entry objects.
+ #
+ class Factory
+ InvalidFactory = Class.new(StandardError)
+
+ def initialize(entry)
+ @entry = entry
+ @metadata = {}
+ @attributes = {}
+ end
+
+ def value(value)
+ @value = value
+ self
+ end
+
+ def metadata(metadata)
+ @metadata.merge!(metadata)
+ self
+ end
+
+ def with(attributes)
+ @attributes.merge!(attributes)
+ self
+ end
+
+ def create!
+ raise InvalidFactory unless defined?(@value)
+
+ ##
+ # We assume that unspecified entry is undefined.
+ # See issue #18775.
+ #
+ if @value.nil?
+ Entry::Unspecified.new(
+ fabricate_unspecified
+ )
+ else
+ fabricate(@entry, @value)
+ end
+ end
+
+ private
+
+ def fabricate_unspecified
+ ##
+ # If entry has a default value we fabricate concrete node
+ # with default value.
+ #
+ if @entry.default.nil?
+ fabricate(Entry::Undefined)
+ else
+ fabricate(@entry, @entry.default)
+ end
+ end
+
+ def fabricate(entry, value = nil)
+ entry.new(value, @metadata).tap do |node|
+ node.key = @attributes[:key]
+ node.parent = @attributes[:parent]
+ node.description = @attributes[:description]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/legacy_validation_helpers.rb b/lib/gitlab/config/entry/legacy_validation_helpers.rb
new file mode 100644
index 00000000000..d3ab5625743
--- /dev/null
+++ b/lib/gitlab/config/entry/legacy_validation_helpers.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ module LegacyValidationHelpers
+ private
+
+ def validate_duration(value)
+ value.is_a?(String) && ChronicDuration.parse(value)
+ rescue ChronicDuration::DurationParseError
+ false
+ end
+
+ def validate_duration_limit(value, limit)
+ return false unless value.is_a?(String)
+
+ ChronicDuration.parse(value).second.from_now <
+ ChronicDuration.parse(limit).second.from_now
+ rescue ChronicDuration::DurationParseError
+ false
+ end
+
+ def validate_array_of_strings(values)
+ values.is_a?(Array) && values.all? { |value| validate_string(value) }
+ end
+
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all? { |value| validate_string_or_regexp(value) }
+ end
+
+ def validate_variables(variables)
+ variables.is_a?(Hash) &&
+ variables.flatten.all? do |value|
+ validate_string(value) || validate_integer(value)
+ end
+ end
+
+ def validate_integer(value)
+ value.is_a?(Integer)
+ end
+
+ def validate_string(value)
+ value.is_a?(String) || value.is_a?(Symbol)
+ end
+
+ def validate_regexp(value)
+ !value.nil? && Regexp.new(value.to_s) && true
+ rescue RegexpError, TypeError
+ false
+ end
+
+ def validate_string_or_regexp(value)
+ return true if value.is_a?(Symbol)
+ return false unless value.is_a?(String)
+
+ if value.first == '/' && value.last == '/'
+ validate_regexp(value[1...-1])
+ else
+ true
+ end
+ end
+
+ def validate_boolean(value)
+ value.in?([true, false])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb
new file mode 100644
index 00000000000..30357b2c95b
--- /dev/null
+++ b/lib/gitlab/config/entry/node.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # Base abstract class for each configuration entry node.
+ #
+ class Node
+ InvalidError = Class.new(StandardError)
+
+ attr_reader :config, :metadata
+ attr_accessor :key, :parent, :description
+
+ def initialize(config, **metadata)
+ @config = config
+ @metadata = metadata
+ @entries = {}
+
+ self.class.aspects.to_a.each do |aspect|
+ instance_exec(&aspect)
+ end
+ end
+
+ def [](key)
+ @entries[key] || Entry::Undefined.new
+ end
+
+ def compose!(deps = nil)
+ return unless valid?
+
+ yield if block_given?
+ end
+
+ def leaf?
+ @entries.none?
+ end
+
+ def descendants
+ @entries.values
+ end
+
+ def ancestors
+ @parent ? @parent.ancestors + [@parent] : []
+ end
+
+ def valid?
+ errors.none?
+ end
+
+ def errors
+ []
+ end
+
+ def value
+ if leaf?
+ @config
+ else
+ meaningful = @entries.select do |_key, value|
+ value.specified? && value.relevant?
+ end
+
+ Hash[meaningful.map { |key, entry| [key, entry.value] }]
+ end
+ end
+
+ def specified?
+ true
+ end
+
+ def relevant?
+ true
+ end
+
+ def location
+ name = @key.presence || self.class.name.to_s.demodulize
+ .underscore.humanize.downcase
+
+ ancestors.map(&:key).append(name).compact.join(':')
+ end
+
+ def inspect
+ val = leaf? ? config : descendants
+ unspecified = specified? ? '' : '(unspecified) '
+ "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>"
+ end
+
+ def self.default
+ end
+
+ def self.aspects
+ @aspects ||= []
+ end
+
+ private
+
+ attr_reader :entries
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb
new file mode 100644
index 00000000000..3e148fe2e91
--- /dev/null
+++ b/lib/gitlab/config/entry/simplifiable.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ class Simplifiable < SimpleDelegator
+ EntryStrategy = Struct.new(:name, :condition)
+
+ def initialize(config, **metadata)
+ unless self.class.const_defined?(:UnknownStrategy)
+ raise ArgumentError, 'UndefinedStrategy not available!'
+ end
+
+ strategy = self.class.strategies.find do |variant|
+ variant.condition.call(config)
+ end
+
+ entry = self.class.entry_class(strategy)
+
+ super(entry.new(config, metadata))
+ end
+
+ def self.strategy(name, **opts)
+ EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy|
+ strategies.append(strategy)
+ end
+ end
+
+ def self.strategies
+ @strategies ||= []
+ end
+
+ def self.entry_class(strategy)
+ if strategy.present?
+ self.const_get(strategy.name)
+ else
+ self::UnknownStrategy
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/undefined.rb b/lib/gitlab/config/entry/undefined.rb
new file mode 100644
index 00000000000..5f708abc80c
--- /dev/null
+++ b/lib/gitlab/config/entry/undefined.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # This class represents an undefined entry.
+ #
+ class Undefined < Node
+ def initialize(*)
+ super(nil)
+ end
+
+ def value
+ nil
+ end
+
+ def valid?
+ true
+ end
+
+ def errors
+ []
+ end
+
+ def specified?
+ false
+ end
+
+ def relevant?
+ false
+ end
+
+ def inspect
+ "#<#{self.class.name}>"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/unspecified.rb b/lib/gitlab/config/entry/unspecified.rb
new file mode 100644
index 00000000000..c096180d0f8
--- /dev/null
+++ b/lib/gitlab/config/entry/unspecified.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ ##
+ # This class represents an unspecified entry.
+ #
+ # It decorates original entry adding method that indicates it is
+ # unspecified.
+ #
+ class Unspecified < SimpleDelegator
+ def specified?
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/validatable.rb b/lib/gitlab/config/entry/validatable.rb
new file mode 100644
index 00000000000..1c88c68c11c
--- /dev/null
+++ b/lib/gitlab/config/entry/validatable.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ module Validatable
+ extend ActiveSupport::Concern
+
+ def self.included(node)
+ node.aspects.append -> do
+ @validator = self.class.validator.new(self)
+ @validator.validate(:new)
+ end
+ end
+
+ def errors
+ @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ class_methods do
+ def validator
+ @validator ||= Class.new(Entry::Validator).tap do |validator|
+ if defined?(@validations)
+ @validations.each { |rules| validator.class_eval(&rules) }
+ end
+ end
+ end
+
+ private
+
+ def validations(&block)
+ (@validations ||= []).append(block)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/validator.rb b/lib/gitlab/config/entry/validator.rb
new file mode 100644
index 00000000000..e5efd4a7b0a
--- /dev/null
+++ b/lib/gitlab/config/entry/validator.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ class Validator < SimpleDelegator
+ include ActiveModel::Validations
+ include Entry::Validators
+
+ def initialize(entry)
+ super(entry)
+ end
+
+ def messages
+ errors.full_messages.map do |error|
+ "#{location} #{error}".downcase
+ end
+ end
+
+ def self.name
+ 'Validator'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
new file mode 100644
index 00000000000..25bfa50f829
--- /dev/null
+++ b/lib/gitlab/config/entry/validators.rb
@@ -0,0 +1,196 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Entry
+ module Validators
+ class AllowedKeysValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unknown_keys = value.try(:keys).to_a - options[:in]
+
+ if unknown_keys.any?
+ record.errors.add(attribute, "contains unknown keys: " +
+ unknown_keys.join(', '))
+ end
+ end
+ end
+
+ class AllowedValuesValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless options[:in].include?(value.to_s)
+ record.errors.add(attribute, "unknown value: #{value}")
+ end
+ end
+ end
+
+ class AllowedArrayValuesValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unkown_values = value - options[:in]
+ unless unkown_values.empty?
+ record.errors.add(attribute, "contains unknown values: " +
+ unkown_values.join(', '))
+ end
+ end
+ end
+
+ class ArrayOfStringsValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings(value)
+ record.errors.add(attribute, 'should be an array of strings')
+ end
+ end
+ end
+
+ class BooleanValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_boolean(value)
+ record.errors.add(attribute, 'should be a boolean value')
+ end
+ end
+ end
+
+ class DurationValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_duration(value)
+ record.errors.add(attribute, 'should be a duration')
+ end
+
+ if options[:limit]
+ unless validate_duration_limit(value, options[:limit])
+ record.errors.add(attribute, 'should not exceed the limit')
+ end
+ end
+ end
+ end
+
+ class HashOrStringValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Hash) || value.is_a?(String)
+ record.errors.add(attribute, 'should be a hash or a string')
+ end
+ end
+ end
+
+ class HashOrIntegerValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Hash) || value.is_a?(Integer)
+ record.errors.add(attribute, 'should be a hash or an integer')
+ end
+ end
+ end
+
+ class KeyValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ if validate_string(value)
+ validate_path(record, attribute, value)
+ else
+ record.errors.add(attribute, 'should be a string or symbol')
+ end
+ end
+
+ private
+
+ def validate_path(record, attribute, value)
+ path = CGI.unescape(value.to_s)
+
+ if path.include?('/')
+ record.errors.add(attribute, 'cannot contain the "/" character')
+ elsif path == '.' || path == '..'
+ record.errors.add(attribute, 'cannot be "." or ".."')
+ end
+ end
+ end
+
+ class RegexpValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_regexp(value)
+ record.errors.add(attribute, 'must be a regular expression')
+ end
+ end
+
+ private
+
+ def look_like_regexp?(value)
+ value.is_a?(String) && value.start_with?('/') &&
+ value.end_with?('/')
+ end
+
+ def validate_regexp(value)
+ look_like_regexp?(value) &&
+ Regexp.new(value.to_s[1...-1]) &&
+ true
+ rescue RegexpError
+ false
+ end
+ end
+
+ class ArrayOfStringsOrRegexpsValidator < RegexpValidator
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings_or_regexps(value)
+ record.errors.add(attribute, 'should be an array of strings or regexps')
+ end
+ end
+
+ private
+
+ def validate_array_of_strings_or_regexps(values)
+ values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp))
+ end
+
+ def validate_string_or_regexp(value)
+ return false unless value.is_a?(String)
+ return validate_regexp(value) if look_like_regexp?(value)
+
+ true
+ end
+ end
+
+ class ArrayOfStringsOrStringValidator < RegexpValidator
+ def validate_each(record, attribute, value)
+ unless validate_array_of_strings_or_string(value)
+ record.errors.add(attribute, 'should be an array of strings or a string')
+ end
+ end
+
+ private
+
+ def validate_array_of_strings_or_string(values)
+ validate_array_of_strings(values) || validate_string(values)
+ end
+ end
+
+ class TypeValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ type = options[:with]
+ raise unless type.is_a?(Class)
+
+ unless value.is_a?(type)
+ message = options[:message] || "should be a #{type.name}"
+ record.errors.add(attribute, message)
+ end
+ end
+ end
+
+ class VariablesValidator < ActiveModel::EachValidator
+ include LegacyValidationHelpers
+
+ def validate_each(record, attribute, value)
+ unless validate_variables(value)
+ record.errors.add(attribute, 'should be a hash of key value pairs')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/config/loader/format_error.rb b/lib/gitlab/config/loader/format_error.rb
new file mode 100644
index 00000000000..848ff96d201
--- /dev/null
+++ b/lib/gitlab/config/loader/format_error.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Config
+ module Loader
+ FormatError = Class.new(StandardError)
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/loader.rb b/lib/gitlab/config/loader/yaml.rb
index b4c491e84a6..8159f8b8026 100644
--- a/lib/gitlab/ci/config/loader.rb
+++ b/lib/gitlab/config/loader/yaml.rb
@@ -1,15 +1,13 @@
# frozen_string_literal: true
module Gitlab
- module Ci
- class Config
- class Loader
- FormatError = Class.new(StandardError)
-
+ module Config
+ module Loader
+ class Yaml
def initialize(config)
@config = YAML.safe_load(config, [Symbol], [], true)
rescue Psych::Exception => e
- raise FormatError, e.message
+ raise Loader::FormatError, e.message
end
def valid?
@@ -18,7 +16,7 @@ module Gitlab
def load!
unless valid?
- raise FormatError, 'Invalid configuration format'
+ raise Loader::FormatError, 'Invalid configuration format'
end
@config.deep_symbolize_keys
diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb
index ea6529e2dc4..c996d786909 100644
--- a/lib/gitlab/database/count.rb
+++ b/lib/gitlab/database/count.rb
@@ -1,7 +1,12 @@
# frozen_string_literal: true
# For large tables, PostgreSQL can take a long time to count rows due to MVCC.
-# We can optimize this by using the reltuples count as described in https://wiki.postgresql.org/wiki/Slow_Counting.
+# We can optimize this by using various strategies for approximate counting.
+#
+# For example, we can use the reltuples count as described in https://wiki.postgresql.org/wiki/Slow_Counting.
+#
+# However, since statistics are not always up to date, we also implement a table sampling strategy
+# that performs an exact count but only on a sample of the table. See TablesampleCountStrategy.
module Gitlab
module Database
module Count
@@ -20,68 +25,30 @@ module Gitlab
end
# Takes in an array of models and returns a Hash for the approximate
- # counts for them. If the model's table has not been vacuumed or
- # analyzed recently, simply run the Model.count to get the data.
+ # counts for them.
+ #
+ # Various count strategies can be specified that are executed in
+ # sequence until all tables have an approximate count attached
+ # or we run out of strategies.
+ #
+ # Note that not all strategies are available on all supported RDBMS.
#
# @param [Array]
# @return [Hash] of Model -> count mapping
- def self.approximate_counts(models)
- table_to_model_map = models.each_with_object({}) do |model, hash|
- hash[model.table_name] = model
- end
-
- table_names = table_to_model_map.keys
- counts_by_table_name = Gitlab::Database.postgresql? ? reltuples_from_recently_updated(table_names) : {}
+ def self.approximate_counts(models, strategies: [TablesampleCountStrategy, ReltuplesCountStrategy, ExactCountStrategy])
+ strategies.each_with_object({}) do |strategy, counts_by_model|
+ if strategy.enabled?
+ models_with_missing_counts = models - counts_by_model.keys
- # Convert table -> count to Model -> count
- counts_by_model = counts_by_table_name.each_with_object({}) do |pair, hash|
- model = table_to_model_map[pair.first]
- hash[model] = pair.second
- end
+ break if models_with_missing_counts.empty?
- missing_tables = table_names - counts_by_table_name.keys
+ counts = strategy.new(models_with_missing_counts).count
- missing_tables.each do |table|
- model = table_to_model_map[table]
- counts_by_model[model] = model.count
+ counts.each do |model, count|
+ counts_by_model[model] = count
+ end
+ end
end
-
- counts_by_model
- end
-
- # Returns a hash of the table names that have recently updated tuples.
- #
- # @param [Array] table names
- # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
- def self.reltuples_from_recently_updated(table_names)
- query = postgresql_estimate_query(table_names)
- rows = []
-
- # Querying tuple stats only works on the primary. Due to load
- # balancing, we need to ensure this query hits the load balancer. The
- # easiest way to do this is to start a transaction.
- ActiveRecord::Base.transaction do
- rows = ActiveRecord::Base.connection.select_all(query)
- end
-
- rows.each_with_object({}) { |row, data| data[row['table_name']] = row['estimate'].to_i }
- rescue *CONNECTION_ERRORS
- {}
- end
-
- # Generates the PostgreSQL query to return the tuples for tables
- # that have been vacuumed or analyzed in the last hour.
- #
- # @param [Array] table names
- # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
- def self.postgresql_estimate_query(table_names)
- time = "to_timestamp(#{1.hour.ago.to_i})"
- <<~SQL
- SELECT pg_class.relname AS table_name, reltuples::bigint AS estimate FROM pg_class
- LEFT JOIN pg_stat_user_tables ON pg_class.relname = pg_stat_user_tables.relname
- WHERE pg_class.relname IN (#{table_names.map { |table| "'#{table}'" }.join(',')})
- AND (last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time})
- SQL
end
end
end
diff --git a/lib/gitlab/database/count/exact_count_strategy.rb b/lib/gitlab/database/count/exact_count_strategy.rb
new file mode 100644
index 00000000000..0276fe2b54f
--- /dev/null
+++ b/lib/gitlab/database/count/exact_count_strategy.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Count
+ # This strategy performs an exact count on the model.
+ #
+ # This is guaranteed to be accurate, however it also scans the
+ # whole table. Hence, there are no guarantees with respect
+ # to runtime.
+ #
+ # Note that for very large tables, this may even timeout.
+ class ExactCountStrategy
+ attr_reader :models
+ def initialize(models)
+ @models = models
+ end
+
+ def count
+ models.each_with_object({}) do |model, data|
+ data[model] = model.count
+ end
+ end
+
+ def self.enabled?
+ true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/count/reltuples_count_strategy.rb b/lib/gitlab/database/count/reltuples_count_strategy.rb
new file mode 100644
index 00000000000..c3a674aeb7e
--- /dev/null
+++ b/lib/gitlab/database/count/reltuples_count_strategy.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Count
+ class PgClass < ActiveRecord::Base
+ self.table_name = 'pg_class'
+ end
+
+ # This strategy counts based on PostgreSQL's statistics in pg_stat_user_tables.
+ #
+ # Specifically, it relies on the column reltuples in said table. An additional
+ # check is performed to make sure statistics were updated within the last hour.
+ #
+ # Otherwise, this strategy skips tables with outdated statistics.
+ #
+ # There are no guarantees with respect to the accuracy of this strategy. Runtime
+ # however is guaranteed to be "fast", because it only looks up statistics.
+ class ReltuplesCountStrategy
+ attr_reader :models
+ def initialize(models)
+ @models = models
+ end
+
+ # Returns a hash of the table names that have recently updated tuples.
+ #
+ # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
+ def count
+ size_estimates
+ rescue *CONNECTION_ERRORS
+ {}
+ end
+
+ def self.enabled?
+ Gitlab::Database.postgresql?
+ end
+
+ private
+
+ def table_names
+ models.map(&:table_name)
+ end
+
+ def size_estimates(check_statistics: true)
+ table_to_model = models.each_with_object({}) { |model, h| h[model.table_name] = model }
+
+ # Querying tuple stats only works on the primary. Due to load balancing, the
+ # easiest way to do this is to start a transaction.
+ ActiveRecord::Base.transaction do
+ get_statistics(table_names, check_statistics: check_statistics).each_with_object({}) do |row, data|
+ model = table_to_model[row.table_name]
+ data[model] = row.estimate
+ end
+ end
+ end
+
+ # Generates the PostgreSQL query to return the tuples for tables
+ # that have been vacuumed or analyzed in the last hour.
+ #
+ # @param [Array] table names
+ # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
+ def get_statistics(table_names, check_statistics: true)
+ time = 1.hour.ago
+
+ query = PgClass.joins("LEFT JOIN pg_stat_user_tables USING (relname)")
+ .where(relname: table_names)
+ .select('pg_class.relname AS table_name, reltuples::bigint AS estimate')
+
+ if check_statistics
+ query = query.where('last_vacuum > ? OR last_autovacuum > ? OR last_analyze > ? OR last_autoanalyze > ?',
+ time, time, time, time)
+ end
+
+ query
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/count/tablesample_count_strategy.rb b/lib/gitlab/database/count/tablesample_count_strategy.rb
new file mode 100644
index 00000000000..cf1cf054dbf
--- /dev/null
+++ b/lib/gitlab/database/count/tablesample_count_strategy.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module Count
+ # A tablesample count executes in two phases:
+ # * Estimate table sizes based on reltuples.
+ # * Based on the estimate:
+ # * If the table is considered 'small', execute an exact relation count.
+ # * Otherwise, count on a sample of the table using TABLESAMPLE.
+ #
+ # The size of the sample is chosen in a way that we always roughly scan
+ # the same amount of rows (see TABLESAMPLE_ROW_TARGET).
+ #
+ # There are no guarantees with respect to the accuracy of the result or runtime.
+ class TablesampleCountStrategy < ReltuplesCountStrategy
+ EXACT_COUNT_THRESHOLD = 10_000
+ TABLESAMPLE_ROW_TARGET = 10_000
+
+ def count
+ estimates = size_estimates(check_statistics: false)
+
+ models.each_with_object({}) do |model, count_by_model|
+ count = perform_count(model, estimates[model])
+ count_by_model[model] = count if count
+ end
+ rescue *CONNECTION_ERRORS
+ {}
+ end
+
+ def self.enabled?
+ Gitlab::Database.postgresql? && Feature.enabled?(:tablesample_counts)
+ end
+
+ private
+
+ def perform_count(model, estimate)
+ # If we estimate 0, we may not have statistics at all. Don't use them.
+ return nil unless estimate && estimate > 0
+
+ if estimate < EXACT_COUNT_THRESHOLD
+ # The table is considered small, the assumption here is that
+ # the exact count will be fast anyways.
+ model.count
+ else
+ # The table is considered large, let's only count on a sample.
+ tablesample_count(model, estimate)
+ end
+ end
+
+ def tablesample_count(model, estimate)
+ portion = (TABLESAMPLE_ROW_TARGET.to_f / estimate).round(4)
+ inverse = 1 / portion
+ query = <<~SQL
+ SELECT (COUNT(*)*#{inverse})::integer AS count
+ FROM #{model.table_name} TABLESAMPLE SYSTEM (#{portion * 100})
+ SQL
+
+ rows = ActiveRecord::Base.connection.select_all(query)
+
+ Integer(rows.first['count'])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index c32c2c0b2fb..22d2d149e65 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -385,7 +385,8 @@ module Gitlab
file_path: encode_binary(action[:file_path]),
previous_path: encode_binary(action[:previous_path]),
base64_content: action[:encoding] == 'base64',
- execute_filemode: !!action[:execute_filemode]
+ execute_filemode: !!action[:execute_filemode],
+ infer_content: !!action[:infer_content]
)
rescue RangeError
raise ArgumentError, "Unknown action '#{action[:action]}'"
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index a497d26312e..2235a6ba194 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -82,7 +82,7 @@ namespace :gettext do
# `gettext:find` writes touches to temp files to `stderr` which would cause
# `static-analysis` to report failures. We can ignore these.
- silence_sdterr do
+ silence_stderr do
Rake::Task['gettext:find'].invoke
end
@@ -119,7 +119,7 @@ namespace :gettext do
end
end
- def silence_sdterr(&block)
+ def silence_stderr(&block)
old_stderr = $stderr.dup
$stderr.reopen(File::NULL)
$stderr.sync = true
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e6cc5ee79a0..60274d883fa 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -303,6 +303,9 @@ msgstr ""
msgid "Abuse reports"
msgstr ""
+msgid "Accept invitation"
+msgstr ""
+
msgid "Accept terms"
msgstr ""
@@ -2262,6 +2265,9 @@ msgstr ""
msgid "December"
msgstr ""
+msgid "Decline"
+msgstr ""
+
msgid "Decline and sign out"
msgstr ""
@@ -3373,6 +3379,9 @@ msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
+msgid "However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation."
+msgstr ""
+
msgid "I accept the %{terms_link}"
msgstr ""
@@ -3448,6 +3457,9 @@ msgstr ""
msgid "ImageDiffViewer|Swipe"
msgstr ""
+msgid "Impersonation has been disabled"
+msgstr ""
+
msgid "Import"
msgstr ""
@@ -3571,6 +3583,9 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
+msgid "Invitation"
+msgstr ""
+
msgid "Invite"
msgstr ""
@@ -4013,6 +4028,18 @@ msgstr ""
msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
+msgid "MergeRequests|started a discussion"
+msgstr ""
+
+msgid "MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}"
+msgstr ""
+
+msgid "MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}"
+msgstr ""
+
+msgid "MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}"
+msgstr ""
+
msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
msgstr ""
@@ -4052,6 +4079,9 @@ msgstr ""
msgid "Metrics|Learn about environments"
msgstr ""
+msgid "Metrics|No data to display"
+msgstr ""
+
msgid "Metrics|No deployed environments"
msgstr ""
@@ -4267,9 +4297,6 @@ msgstr ""
msgid "No"
msgstr ""
-msgid "No Label"
-msgstr ""
-
msgid "No assignee"
msgstr ""
@@ -4363,6 +4390,9 @@ msgstr ""
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr ""
+msgid "Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}."
+msgstr ""
+
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
@@ -7630,6 +7660,9 @@ msgstr ""
msgid "from"
msgstr ""
+msgid "group"
+msgstr ""
+
msgid "here"
msgstr ""
@@ -7876,6 +7909,9 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "project"
+msgstr ""
+
msgid "remaining"
msgstr ""
diff --git a/package.json b/package.json
index e573fb1ecc0..520d7c620c2 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,7 @@
"chart.js": "1.0.2",
"classlist-polyfill": "^1.2.0",
"clipboard": "^1.7.1",
- "codesandbox-api": "^0.0.18",
+ "codesandbox-api": "^0.0.20",
"compression-webpack-plugin": "^2.0.0",
"core-js": "^2.4.1",
"cropper": "^2.3.0",
@@ -86,7 +86,7 @@
"sanitize-html": "^1.16.1",
"select2": "3.5.2-browserify",
"sha1": "^1.1.1",
- "smooshpack": "^0.0.48",
+ "smooshpack": "^0.0.53",
"sortablejs": "^1.7.0",
"sql.js": "^0.4.0",
"stickyfilljs": "^2.0.5",
diff --git a/public/robots.txt b/public/robots.txt
index ea931e1a223..7908297564e 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -70,3 +70,5 @@ Disallow: /*/*/hooks
Disallow: /*/*/services
Disallow: /*/*/protected_branches
Disallow: /*/*/uploads/
+Disallow: /*/-/group_members
+Disallow: /*/project_members
diff --git a/scripts/trigger-build b/scripts/trigger-build
index d2c71f5be3e..14af3281106 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -68,6 +68,7 @@ module Trigger
def base_variables
{
+ 'GITLAB_REF_SLUG' => ref_slug,
'TRIGGERED_USER' => ENV['TRIGGERED_USER'] || ENV['GITLAB_USER_NAME'],
'TRIGGER_SOURCE' => ENV['CI_JOB_URL'],
'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'],
@@ -76,6 +77,12 @@ module Trigger
}
end
+ def ref_slug
+ return 'master' if ENV['CI_COMMIT_REF_SLUG'] =~ %r{(\Aqa[/-]|-qa\z)}
+
+ ENV['CI_COMMIT_REF_SLUG']
+ end
+
# Read version files from all components
def version_file_variables
Dir.glob("*_VERSION").each_with_object({}) do |version_file, params|
@@ -106,18 +113,11 @@ module Trigger
def extra_variables
{
'GITLAB_VERSION' => ENV['CI_COMMIT_SHA'],
- 'GITLAB_REF_SLUG' => ref_slug,
'ALTERNATIVE_SOURCES' => 'true',
'ee' => Trigger.ee? ? 'true' : 'false',
'QA_BRANCH' => ENV['QA_BRANCH'] || 'master'
}
end
-
- def ref_slug
- return 'master' if ENV['CI_COMMIT_REF_SLUG'] =~ %r{(\Aqa[/-]|-qa\z)}
-
- ENV['CI_COMMIT_REF_SLUG']
- end
end
class CNG < Base
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index f350641a643..3dd0b2623ac 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -264,5 +264,17 @@ describe Admin::UsersController do
expect(flash[:alert]).to eq("You are now impersonating #{user.username}")
end
end
+
+ context "when impersonation is disabled" do
+ before do
+ stub_config_setting(impersonation_enabled: false)
+ end
+
+ it "shows error page" do
+ post :impersonate, id: user.username
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 1b585bcd4c6..ac92b2ca657 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -114,7 +114,7 @@ describe ApplicationController do
skip_before_action :authenticate_user!, only: :index
def index
- render text: 'authenticated'
+ render html: 'authenticated'
end
end
@@ -401,7 +401,7 @@ describe ApplicationController do
context 'terms' do
controller(described_class) do
def index
- render text: 'authenticated'
+ render html: 'authenticated'
end
end
@@ -444,7 +444,7 @@ describe ApplicationController do
attr_reader :last_payload
def index
- render text: 'authenticated'
+ render html: 'authenticated'
end
def append_info_to_payload(payload)
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index bc17331f531..5fa0488014f 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -217,7 +217,10 @@ describe Projects::EnvironmentsController do
end
it 'loads the terminals for the environment' do
- expect_any_instance_of(Environment).to receive(:terminals)
+ # In EE we have to stub EE::Environment since it overwrites the
+ # "terminals" method.
+ expect_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ .to receive(:terminals)
get :terminal, environment_params
end
@@ -240,7 +243,9 @@ describe Projects::EnvironmentsController do
context 'and valid id' do
it 'returns the first terminal for the environment' do
- expect_any_instance_of(Environment)
+ # In EE we have to stub EE::Environment since it overwrites the
+ # "terminals" method.
+ expect_any_instance_of(defined?(EE) ? EE::Environment : Environment)
.to receive(:terminals)
.and_return([:fake_terminal])
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index f7c7a257075..d5516b334b9 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -205,75 +205,118 @@ describe "Admin::Users" do
describe 'Impersonation' do
let(:another_user) { create(:user) }
- before do
- visit admin_user_path(another_user)
- end
-
context 'before impersonating' do
- it 'shows impersonate button for other users' do
- expect(page).to have_content('Impersonate')
+ subject { visit admin_user_path(user_to_visit) }
+
+ let(:user_to_visit) { another_user }
+
+ context 'for other users' do
+ it 'shows impersonate button for other users' do
+ subject
+
+ expect(page).to have_content('Impersonate')
+ end
end
- it 'does not show impersonate button for admin itself' do
- visit admin_user_path(current_user)
+ context 'for admin itself' do
+ let(:user_to_visit) { current_user }
- expect(page).not_to have_content('Impersonate')
+ it 'does not show impersonate button for admin itself' do
+ subject
+
+ expect(page).not_to have_content('Impersonate')
+ end
end
- it 'does not show impersonate button for blocked user' do
- another_user.block
+ context 'for blocked user' do
+ before do
+ another_user.block
+ end
- visit admin_user_path(another_user)
+ it 'does not show impersonate button for blocked user' do
+ subject
- expect(page).not_to have_content('Impersonate')
+ expect(page).not_to have_content('Impersonate')
+ end
+ end
+
+ context 'when impersonation is disabled' do
+ before do
+ stub_config_setting(impersonation_enabled: false)
+ end
- another_user.activate
+ it 'does not show impersonate button' do
+ subject
+
+ expect(page).not_to have_content('Impersonate')
+ end
end
end
context 'when impersonating' do
+ subject { click_link 'Impersonate' }
+
before do
- click_link 'Impersonate'
+ visit admin_user_path(another_user)
end
it 'logs in as the user when impersonate is clicked' do
+ subject
+
expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username)
end
it 'sees impersonation log out icon' do
- icon = first('.fa.fa-user-secret')
+ subject
+ icon = first('.fa.fa-user-secret')
expect(icon).not_to be nil
end
- it 'logs out of impersonated user back to original user' do
- find(:css, 'li.impersonation a').click
-
- expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username)
- end
+ context 'a user with an expired password' do
+ before do
+ another_user.update(password_expires_at: Time.now - 5.minutes)
+ end
- it 'is redirected back to the impersonated users page in the admin after stopping' do
- find(:css, 'li.impersonation a').click
+ it 'does not redirect to password change page' do
+ subject
- expect(current_path).to eq("/admin/users/#{another_user.username}")
+ expect(current_path).to eq('/')
+ end
end
end
- context 'when impersonating a user with an expired password' do
+ context 'ending impersonation' do
+ subject { find(:css, 'li.impersonation a').click }
+
before do
- another_user.update(password_expires_at: Time.now - 5.minutes)
+ visit admin_user_path(another_user)
click_link 'Impersonate'
end
- it 'does not redirect to password change page' do
- expect(current_path).to eq('/')
+ it 'logs out of impersonated user back to original user' do
+ subject
+
+ expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username)
end
it 'is redirected back to the impersonated users page in the admin after stopping' do
- find(:css, 'li.impersonation a').click
+ subject
expect(current_path).to eq("/admin/users/#{another_user.username}")
end
+
+ context 'a user with an expired password' do
+ before do
+ another_user.update(password_expires_at: Time.now - 5.minutes)
+ end
+
+ it 'is redirected back to the impersonated users page in the admin after stopping' do
+ subject
+
+ expect(current_path).to eq("/admin/users/#{another_user.username}")
+ end
+ end
end
end
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 2cdd3f55b50..d96707e55fd 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -176,7 +176,7 @@ describe 'Issue Boards add issue modal filtering', :js do
it 'filters by no label' do
set_filter('label')
- click_filter_link('No Label')
+ click_filter_link('None')
submit_filter
page.within('.add-issues-modal') do
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index ca5d506ab04..b25b1514d62 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -45,7 +45,8 @@ describe 'Dropdown label', :js do
bug_label = create(:label, project: project, title: 'bug-label')
init_label_search
- filtered_search.native.send_keys(:down, :down, :enter)
+ # navigate to the bug_label option and selects it
+ filtered_search.native.send_keys(:down, :down, :down, :enter)
expect_tokens([label_token(bug_label.title)])
expect_filtered_search_input_empty
@@ -234,12 +235,20 @@ describe 'Dropdown label', :js do
end
it 'selects `no label`' do
- find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
+ find("#{js_dropdown_label} .filter-dropdown-item", text: 'None').click
expect(page).not_to have_css(js_dropdown_label)
expect_tokens([label_token('none', false)])
expect_filtered_search_input_empty
end
+
+ it 'selects `any label`' do
+ find("#{js_dropdown_label} .filter-dropdown-item", text: 'Any').click
+
+ expect(page).not_to have_css(js_dropdown_label)
+ expect_tokens([label_token('any', false)])
+ expect_filtered_search_input_empty
+ end
end
describe 'input has existing content' do
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 35d57b3896d..4d9b8262f21 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -430,10 +430,10 @@ describe 'Filter issues', :js do
expect_issues_list_count(2)
- sort_toggle = find('.filtered-search-wrapper .dropdown-toggle')
+ sort_toggle = find('.filter-dropdown-container .dropdown-menu-toggle')
sort_toggle.click
- find('.filtered-search-wrapper .dropdown-menu li a', text: 'Created date').click
+ find('.filter-dropdown-container .dropdown-menu li a', text: 'Created date').click
wait_for_requests
expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(new_issue.title)
diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb
index 4771d2c6d28..3bc93933183 100644
--- a/spec/features/issues/user_sorts_issues_spec.rb
+++ b/spec/features/issues/user_sorts_issues_spec.rb
@@ -20,7 +20,7 @@ describe "User sorts issues" do
end
it 'keeps the sort option' do
- find('button.dropdown-toggle').click
+ find('.filter-dropdown-container button.dropdown-menu-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
@@ -40,7 +40,7 @@ describe "User sorts issues" do
end
it "sorts by popularity" do
- find("button.dropdown-toggle").click
+ find(".filter-dropdown-container button.dropdown-menu-toggle").click
page.within(".content ul.dropdown-menu.dropdown-menu-right li") do
click_link("Popularity")
diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
index 3e40179ad9a..fe8e0b07d39 100644
--- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb
@@ -29,6 +29,22 @@ describe 'Merge request > User sees deployment widget', :js do
expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.js-deploy-time')['data-original-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
end
+
+ context 'when a user created a new merge request with the same SHA' do
+ let(:pipeline2) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: 'new-patch-1') }
+ let(:build2) { create(:ci_build, :success, pipeline: pipeline2) }
+ let(:environment2) { create(:environment, project: project) }
+ let!(:deployment2) { create(:deployment, environment: environment2, sha: sha, ref: 'new-patch-1', deployable: build2) }
+
+ it 'displays one environment which is related to the pipeline' do
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ expect(page).to have_selector('.js-deployment-info', count: 1)
+ expect(page).to have_content("#{environment.name}")
+ expect(page).not_to have_content("#{environment2.name}")
+ end
+ end
end
context 'when deployment failed' do
diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb
index 7b8c3bacfe2..4ab9a87ad4b 100644
--- a/spec/features/merge_request/user_sees_discussions_spec.rb
+++ b/spec/features/merge_request/user_sees_discussions_spec.rb
@@ -53,13 +53,11 @@ describe 'Merge request > User sees discussions', :js do
shared_examples 'a functional discussion' do
let(:discussion_id) { note.discussion_id(merge_request) }
- # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
- xit 'is displayed' do
+ it 'is displayed' do
expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']")
end
- # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
- xit 'can be replied to' do
+ it 'can be replied to' do
within(".discussion[data-discussion-id='#{discussion_id}']") do
click_button 'Reply...'
fill_in 'note[note]', with: 'Test!'
@@ -74,16 +72,21 @@ describe 'Merge request > User sees discussions', :js do
visit project_merge_request_path(project, merge_request)
end
- context 'a regular commit comment' do
- let(:note) { create(:note_on_commit, project: project) }
-
- it_behaves_like 'a functional discussion'
- end
+ # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+ # context 'a regular commit comment' do
+ # let(:note) { create(:note_on_commit, project: project) }
+ #
+ # it_behaves_like 'a functional discussion'
+ # end
context 'a commit diff comment' do
let(:note) { create(:diff_note_on_commit, project: project) }
it_behaves_like 'a functional discussion'
+
+ it 'displays correct header' do
+ expect(page).to have_content "started a discussion on commit #{note.commit_id[0...7]}"
+ end
end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 582be101399..d8ebd3c92af 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -60,7 +60,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows environments link' do
wait_for_requests
- page.within('.js-pre-merge-deploy') do
+ page.within('.js-pre-deployment') do
expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url)
end
diff --git a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
index e163868e8e7..61e8f1c4662 100644
--- a/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_sorts_merge_requests_spec.rb
@@ -19,7 +19,7 @@ describe 'User sorts merge requests' do
end
it 'keeps the sort option' do
- find('button.dropdown-toggle').click
+ find('.filter-dropdown-container button.dropdown-menu-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
@@ -49,7 +49,7 @@ describe 'User sorts merge requests' do
it 'separates remember sorting with issues' do
create(:issue, project: project)
- find('button.dropdown-toggle').click
+ find('.filter-dropdown-container button.dropdown-menu-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Milestone')
@@ -70,7 +70,7 @@ describe 'User sorts merge requests' do
end
it 'sorts by popularity' do
- find('button.dropdown-toggle').click
+ find('.filter-dropdown-container button.dropdown-menu-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link('Popularity')
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 9772a7bacac..a8a3b6910fb 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -165,8 +165,14 @@ describe 'Environment' do
context 'web terminal', :js do
before do
- # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
- allow_any_instance_of(Environment).to receive(:terminals) { nil }
+ # Stub #terminals as it causes js-enabled feature specs to
+ # render the page incorrectly
+ #
+ # In EE we have to stub EE::Environment since it overwrites
+ # the "terminals" method.
+ allow_any_instance_of(defined?(EE) ? EE::Environment : Environment)
+ .to receive(:terminals) { nil }
+
visit terminal_project_environment_path(project, environment)
end
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
index 6178f11ded7..b778c72bc76 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -32,7 +32,7 @@ describe 'Issue prioritization' do
visit project_issues_path(project, sort: 'label_priority')
# Ensure we are indicating that issues are sorted by priority
- expect(page).to have_selector('.dropdown-toggle', text: 'Label priority')
+ expect(page).to have_selector('.dropdown-menu-toggle', text: 'Label priority')
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
@@ -70,7 +70,7 @@ describe 'Issue prioritization' do
sign_in user
visit project_issues_path(project, sort: 'label_priority')
- expect(page).to have_selector('.dropdown-toggle', text: 'Label priority')
+ expect(page).to have_selector('.dropdown-menu-toggle', text: 'Label priority')
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 401aac9478d..b7a22316d26 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -133,19 +133,50 @@ describe 'Projects > Settings > Repository settings' do
expect(page).to have_selector('#mirror_direction')
end
- it 'generates an SSH public key on submission', :js do
+ it 'creates a push mirror that mirrors all branches', :js do
+ expect(find('.js-mirror-protected-hidden', visible: false).value).to eq('0')
+
fill_in 'url', with: 'ssh://user@localhost/project.git'
select 'SSH public key', from: 'Authentication method'
- direction_select = find('#mirror_direction')
+ select_direction
- # In CE, this select box is disabled, but in EE, it is enabled
- if direction_select.disabled?
- expect(direction_select.value).to eq('push')
- else
- direction_select.select('Push')
+ Sidekiq::Testing.fake! do
+ click_button 'Mirror repository'
end
+ project.reload
+
+ expect(page).to have_content('Mirroring settings were successfully updated')
+ expect(project.remote_mirrors.first.only_protected_branches).to eq(false)
+ end
+
+ it 'creates a push mirror that only mirrors protected branches', :js do
+ find('#only_protected_branches').click
+
+ expect(find('.js-mirror-protected-hidden', visible: false).value).to eq('1')
+
+ fill_in 'url', with: 'ssh://user@localhost/project.git'
+ select 'SSH public key', from: 'Authentication method'
+
+ select_direction
+
+ Sidekiq::Testing.fake! do
+ click_button 'Mirror repository'
+ end
+
+ project.reload
+
+ expect(page).to have_content('Mirroring settings were successfully updated')
+ expect(project.remote_mirrors.first.only_protected_branches).to eq(true)
+ end
+
+ it 'generates an SSH public key on submission', :js do
+ fill_in 'url', with: 'ssh://user@localhost/project.git'
+ select 'SSH public key', from: 'Authentication method'
+
+ select_direction
+
Sidekiq::Testing.fake! do
click_button 'Mirror repository'
end
@@ -153,6 +184,17 @@ describe 'Projects > Settings > Repository settings' do
expect(page).to have_content('Mirroring settings were successfully updated')
expect(page).to have_selector('[title="Copy SSH public key"]')
end
+
+ def select_direction(direction = 'push')
+ direction_select = find('#mirror_direction')
+
+ # In CE, this select box is disabled, but in EE, it is enabled
+ if direction_select.disabled?
+ expect(direction_select.value).to eq(direction)
+ else
+ direction_select.select(direction.capitalize)
+ end
+ end
end
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 571104d3467..48a0d675f2d 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -85,7 +85,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("test").and have_content("Create Page")
end
click_link("Home")
@@ -97,7 +97,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").and have_content("api")
end
click_link("Home")
@@ -109,7 +109,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").and have_content("rake")
end
end
@@ -200,7 +200,7 @@ describe "User creates wiki page" do
click_button("Create page")
end
- expect(page).to have_content("Foo")
+ expect(page).to have_content("foo")
.and have_content("Last edited by #{user.name}")
.and have_content("My awesome wiki!")
end
@@ -215,7 +215,7 @@ describe "User creates wiki page" do
end
# Commit message field should have correct value.
- expect(page).to have_field("wiki[message]", with: "Create spaces in the name")
+ 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!")
@@ -246,7 +246,7 @@ describe "User creates wiki page" do
click_button("Create page")
end
- expect(page).to have_content("Hyphens in the name")
+ expect(page).to have_content("hyphens in the name")
.and have_content("Last edited by #{user.name}")
.and have_content("My awesome wiki!")
end
@@ -293,7 +293,7 @@ describe "User creates wiki page" do
click_button("Create page")
end
- expect(page).to have_content("Foo")
+ expect(page).to have_content("foo")
.and have_content("Last edited by #{user.name}")
.and have_content("My awesome wiki!")
end
@@ -311,7 +311,7 @@ describe "User creates wiki page" do
it 'renders a default sidebar when there is no customized sidebar' do
visit(project_wikis_path(project))
- expect(page).to have_content('Another')
+ expect(page).to have_content('another')
expect(page).to have_content('More Pages')
end
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 7ad7fec922a..f76e577b0d6 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -39,11 +39,11 @@ describe 'User updates wiki page' do
end
expect(current_path).to include('one/two/three-test')
- expect(find('.wiki-pages')).to have_content('Three')
+ expect(find('.wiki-pages')).to have_content('three')
- first(:link, text: 'Three').click
+ first(:link, text: 'three').click
- expect(find('.nav-text')).to have_content('Three')
+ expect(find('.nav-text')).to have_content('three')
click_on('Edit')
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 3c93d71ab00..d4691b669c1 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -38,7 +38,7 @@ describe 'User views a wiki page' do
it 'shows the history of a page that has a path', :js do
expect(current_path).to include('one/two/three-test')
- first(:link, text: 'Three').click
+ first(:link, text: 'three').click
click_on('Page history')
expect(current_path).to include('one/two/three-test')
@@ -50,11 +50,11 @@ describe 'User views a wiki page' do
it 'shows an old version of a page', :js do
expect(current_path).to include('one/two/three-test')
- expect(find('.wiki-pages')).to have_content('Three')
+ expect(find('.wiki-pages')).to have_content('three')
- first(:link, text: 'Three').click
+ first(:link, text: 'three').click
- expect(find('.nav-text')).to have_content('Three')
+ expect(find('.nav-text')).to have_content('three')
click_on('Edit')
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
index 4b47e259c0f..314f04107eb 100644
--- a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json
+++ b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json
@@ -1,46 +1,154 @@
[
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2012-4387",
- "url": "http://struts.apache.org/docs/s2-011.html",
- "message": "Long parameter name DoS for org.apache.struts/struts2-core",
- "tools": [
- "gemnasium"
+ "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"
+ },
+ "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"
},
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2013-1966",
- "url": "http://struts.apache.org/docs/s2-014.html",
- "message": "Remote command execution due to flaw in the includeParams attribute of URL and Anchor tags for org.apache.struts/struts2-core",
- "tools": [
- "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"
+ },
+ "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"
},
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2013-2115",
- "url": "http://struts.apache.org/docs/s2-014.html",
- "message": "Remote command execution due to flaw in the includeParams attribute of URL and Anchor tags for org.apache.struts/struts2-core",
- "tools": [
- "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"
+ },
+ "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"
},
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2013-2134",
- "url": "http://struts.apache.org/docs/s2-015.html",
- "message": "Arbitrary OGNL code execution via unsanitized wildcard matching for org.apache.struts/struts2-core",
- "tools": [
- "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"
+ },
+ "identifiers": [
+ {
+ "type": "cve",
+ "name": "CVE-2018-1000201",
+ "value": "CVE-2018-1000201",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201"
+ }
],
- "tool": "gemnasium"
+ "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-dependency-scanning-report.json b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
index b4e4e8e7dd5..314f04107eb 100644
--- a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
+++ b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json
@@ -1,35 +1,154 @@
[
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2012-4386",
- "url": "http://struts.apache.org/docs/s2-010.html",
- "message": "CSRF protection bypass for org.apache.struts/struts2-core",
- "tools": [
- "gemnasium"
+ "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"
+ },
+ "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"
},
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2012-4387",
- "url": "http://struts.apache.org/docs/s2-011.html",
- "message": "Long parameter name DoS for org.apache.struts/struts2-core",
- "tools": [
- "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"
+ },
+ "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"
},
{
- "priority": "Unknown",
- "file": "pom.xml",
- "cve": "CVE-2013-1966",
- "url": "http://struts.apache.org/docs/s2-014.html",
- "message": "Remote command execution due to flaw in the includeParams attribute of URL and Anchor tags for org.apache.struts/struts2-core",
- "tools": [
- "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"
+ },
+ "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"
+ },
+ "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/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js
index 1a0b7612ee9..1e2f7ff4fd8 100644
--- a/spec/javascripts/diffs/components/app_spec.js
+++ b/spec/javascripts/diffs/components/app_spec.js
@@ -13,6 +13,8 @@ describe('diffs/components/app', () => {
beforeEach(() => {
// setup globals (needed for component to mount :/)
window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
+ window.mrTabs.expandViewContainer = jasmine.createSpy();
+ window.location.hash = 'ABC_123';
// setup component
const store = createDiffsStore();
@@ -39,4 +41,15 @@ describe('diffs/components/app', () => {
it('does not show commit info', () => {
expect(vm.$el).not.toContainElement('.blob-commit-info');
});
+
+ it('sets highlighted row if hash exists in location object', done => {
+ vm.$props.shouldShow = true;
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$store.state.diffs.highlightedRow).toBe('ABC_123');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
diff --git a/spec/javascripts/diffs/components/diff_table_cell_spec.js b/spec/javascripts/diffs/components/diff_table_cell_spec.js
new file mode 100644
index 00000000000..170e661beea
--- /dev/null
+++ b/spec/javascripts/diffs/components/diff_table_cell_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import store from '~/mr_notes/stores';
+import DiffTableCell from '~/diffs/components/diff_table_cell.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffTableCell', () => {
+ const createComponent = options =>
+ createComponentWithStore(Vue.extend(DiffTableCell), store, {
+ line: diffFileMockData.highlighted_diff_lines[0],
+ fileHash: diffFileMockData.file_hash,
+ contextLinesPath: 'contextLinesPath',
+ ...options,
+ }).$mount();
+
+ it('does not highlight row when isHighlighted prop is false', done => {
+ const vm = createComponent({ isHighlighted: false });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.classList).not.toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights row when isHighlighted prop is true', done => {
+ const vm = createComponent({ isHighlighted: true });
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.classList).toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+});
diff --git a/spec/javascripts/diffs/components/inline_diff_table_row_spec.js b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js
new file mode 100644
index 00000000000..97926f6625e
--- /dev/null
+++ b/spec/javascripts/diffs/components/inline_diff_table_row_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import store from '~/mr_notes/stores';
+import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('InlineDiffTableRow', () => {
+ let vm;
+ const thisLine = diffFileMockData.highlighted_diff_lines[0];
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Vue.extend(InlineDiffTableRow), store, {
+ line: thisLine,
+ fileHash: diffFileMockData.file_hash,
+ contextLinesPath: 'contextLinesPath',
+ isHighlighted: false,
+ }).$mount();
+ });
+
+ it('does not add hll class to line content when line does not match highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content').classList).not.toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('adds hll class to lineContent when line is the highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ vm.$store.state.diffs.highlightedRow = thisLine.line_code;
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content').classList).toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+});
diff --git a/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js b/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js
new file mode 100644
index 00000000000..311eaaaa7c8
--- /dev/null
+++ b/spec/javascripts/diffs/components/parallel_diff_table_row_spec.js
@@ -0,0 +1,85 @@
+import Vue from 'vue';
+import { createStore } from '~/mr_notes/stores';
+import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('ParallelDiffTableRow', () => {
+ describe('when one side is empty', () => {
+ let vm;
+ const thisLine = diffFileMockData.parallel_diff_lines[0];
+ const rightLine = diffFileMockData.parallel_diff_lines[0].right;
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), {
+ line: thisLine,
+ fileHash: diffFileMockData.file_hash,
+ contextLinesPath: 'contextLinesPath',
+ isHighlighted: false,
+ }).$mount();
+ });
+
+ it('does not highlight non empty line content when line does not match highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('highlights nonempty line content when line is the highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ vm.$store.state.diffs.highlightedRow = rightLine.line_code;
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('when both sides have content', () => {
+ let vm;
+ const thisLine = diffFileMockData.parallel_diff_lines[2];
+ const rightLine = diffFileMockData.parallel_diff_lines[2].right;
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Vue.extend(ParallelDiffTableRow), createStore(), {
+ line: thisLine,
+ fileHash: diffFileMockData.file_hash,
+ contextLinesPath: 'contextLinesPath',
+ isHighlighted: false,
+ }).$mount();
+ });
+
+ it('does not highlight either line when line does not match highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content.right-side').classList).not.toContain('hll');
+ expect(vm.$el.querySelector('.line_content.left-side').classList).not.toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('adds hll class to lineContent when line is the highlighted row', done => {
+ vm.$nextTick()
+ .then(() => {
+ vm.$store.state.diffs.highlightedRow = rightLine.line_code;
+
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.line_content.right-side').classList).toContain('hll');
+ expect(vm.$el.querySelector('.line_content.left-side').classList).toContain('hll');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 43d8d950bed..205138bd845 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -22,6 +22,7 @@ import actions, {
expandAllFiles,
toggleFileDiscussions,
saveDiffDiscussion,
+ setHighlightedRow,
toggleTreeOpen,
scrollToFile,
toggleShowTreeList,
@@ -92,6 +93,14 @@ describe('DiffsStoreActions', () => {
});
});
+ describe('setHighlightedRow', () => {
+ it('should set lineHash and fileHash of highlightedRow', () => {
+ testAction(setHighlightedRow, 'ABC_123', {}, [
+ { type: types.SET_HIGHLIGHTED_ROW, payload: 'ABC_123' },
+ ]);
+ });
+ });
+
describe('assignDiscussionsToDiff', () => {
it('should merge discussions into diffs', done => {
const state = {
@@ -469,7 +478,7 @@ describe('DiffsStoreActions', () => {
describe('scrollToLineIfNeededInline', () => {
const lineMock = {
- lineCode: 'ABC_123',
+ line_code: 'ABC_123',
};
it('should not call handleLocationHash when there is not hash', () => {
@@ -520,7 +529,7 @@ describe('DiffsStoreActions', () => {
const lineMock = {
left: null,
right: {
- lineCode: 'ABC_123',
+ line_code: 'ABC_123',
},
};
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index d1040ace5ca..7a06c178f0b 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -360,6 +360,16 @@ describe('DiffsStoreMutations', () => {
});
});
+ describe('Set highlighted row', () => {
+ it('sets highlighted row', () => {
+ const state = createState();
+
+ mutations[types.SET_HIGHLIGHTED_ROW](state, 'ABC_123');
+
+ expect(state.highlightedRow).toBe('ABC_123');
+ });
+ });
+
describe('TOGGLE_LINE_HAS_FORM', () => {
it('sets hasForm on lines', () => {
const file = {
diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/javascripts/lib/utils/url_utility_spec.js
index c7f4092911c..e4df8441793 100644
--- a/spec/javascripts/lib/utils/url_utility_spec.js
+++ b/spec/javascripts/lib/utils/url_utility_spec.js
@@ -1,4 +1,4 @@
-import { webIDEUrl } from '~/lib/utils/url_utility';
+import { webIDEUrl, mergeUrlParams } from '~/lib/utils/url_utility';
describe('URL utility', () => {
describe('webIDEUrl', () => {
@@ -26,4 +26,26 @@ describe('URL utility', () => {
});
});
});
+
+ describe('mergeUrlParams', () => {
+ it('adds w', () => {
+ expect(mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag');
+ expect(mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag');
+ expect(mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1');
+ expect(mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe('https://host/path?w=1#frag');
+ expect(mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe('https://h/p?k1=v1&w=1#frag');
+ });
+
+ it('updates w', () => {
+ expect(mergeUrlParams({ w: 1 }, '?k1=v1&w=0#frag')).toBe('?k1=v1&w=1#frag');
+ });
+
+ it('adds multiple params', () => {
+ expect(mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag');
+ });
+
+ it('adds and updates encoded params', () => {
+ expect(mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag');
+ });
+ });
});
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index 4cc18afdf24..59d6d4f3a7f 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -5,6 +5,7 @@ import {
deploymentData,
convertDatesMultipleSeries,
singleRowMetricsMultipleSeries,
+ queryWithoutData,
} from './mock_data';
const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags';
@@ -104,4 +105,23 @@ describe('Graph', () => {
expect(component.currentData).toBe(component.timeSeries[0].values[10]);
});
+
+ describe('Without data to display', () => {
+ it('shows a "no data to display" empty state on a graph', done => {
+ const component = createComponent({
+ graphData: queryWithoutData,
+ deploymentData,
+ tagsPath,
+ projectPath,
+ });
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.js-no-data-to-display text').textContent.trim(),
+ ).toEqual('No data to display');
+
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 6c833b17f98..18ad9843d22 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -14,7 +14,7 @@ export const metricsGroupsAPIResponse = {
queries: [
{
query_range: 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20',
- y_label: 'Memory',
+ label: 'Memory',
unit: 'MiB',
result: [
{
@@ -324,12 +324,15 @@ export const metricsGroupsAPIResponse = {
],
},
{
+ id: 6,
title: 'CPU usage',
weight: 1,
queries: [
{
query_range:
'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100',
+ label: 'Core Usage',
+ unit: 'Cores',
result: [
{
metric: {},
@@ -639,6 +642,39 @@ export const metricsGroupsAPIResponse = {
},
],
},
+ {
+ group: 'NGINX',
+ priority: 2,
+ metrics: [
+ {
+ id: 100,
+ title: 'Http Error Rate',
+ weight: 100,
+ queries: [
+ {
+ query_range:
+ 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100',
+ label: '5xx errors',
+ unit: '%',
+ result: [
+ {
+ metric: {},
+ values: [
+ [1495700554.925, NaN],
+ [1495700614.925, NaN],
+ [1495700674.925, NaN],
+ [1495700734.925, NaN],
+ [1495700794.925, NaN],
+ [1495700854.925, NaN],
+ [1495700914.925, NaN],
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
],
last_update: '2017-05-25T13:18:34.949Z',
};
@@ -6526,6 +6562,21 @@ export const singleRowMetricsMultipleSeries = [
},
];
+export const queryWithoutData = {
+ title: 'HTTP Error rate',
+ weight: 10,
+ y_label: 'Http Error Rate',
+ queries: [
+ {
+ query_range:
+ 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"nginx-test-8691397-production-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"nginx-test-8691397-production-.*"}[2m])) * 100',
+ label: '5xx errors',
+ unit: '%',
+ result: [],
+ },
+ ],
+};
+
export function convertDatesMultipleSeries(multipleSeries) {
const convertedMultiple = multipleSeries;
multipleSeries.forEach((column, index) => {
diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js
index bf68c911549..d8a980c874d 100644
--- a/spec/javascripts/monitoring/monitoring_store_spec.js
+++ b/spec/javascripts/monitoring/monitoring_store_spec.js
@@ -1,31 +1,35 @@
import MonitoringStore from '~/monitoring/stores/monitoring_store';
import MonitoringMock, { deploymentData, environmentData } from './mock_data';
-describe('MonitoringStore', function() {
- this.store = new MonitoringStore();
- this.store.storeMetrics(MonitoringMock.data);
-
- it('contains one group that contains two queries sorted by priority', () => {
- expect(this.store.groups).toBeDefined();
- expect(this.store.groups.length).toEqual(1);
- expect(this.store.groups[0].metrics.length).toEqual(2);
+describe('MonitoringStore', () => {
+ const store = new MonitoringStore();
+ store.storeMetrics(MonitoringMock.data);
+
+ it('contains two groups that contains, one of which has two queries sorted by priority', () => {
+ expect(store.groups).toBeDefined();
+ expect(store.groups.length).toEqual(2);
+ expect(store.groups[0].metrics.length).toEqual(2);
});
it('gets the metrics count for every group', () => {
- expect(this.store.getMetricsCount()).toEqual(2);
+ expect(store.getMetricsCount()).toEqual(3);
});
it('contains deployment data', () => {
- this.store.storeDeploymentData(deploymentData);
+ store.storeDeploymentData(deploymentData);
- expect(this.store.deploymentData).toBeDefined();
- expect(this.store.deploymentData.length).toEqual(3);
- expect(typeof this.store.deploymentData[0]).toEqual('object');
+ expect(store.deploymentData).toBeDefined();
+ expect(store.deploymentData.length).toEqual(3);
+ expect(typeof store.deploymentData[0]).toEqual('object');
});
it('only stores environment data that contains deployments', () => {
- this.store.storeEnvironmentsData(environmentData);
+ store.storeEnvironmentsData(environmentData);
+
+ expect(store.environmentsData.length).toEqual(2);
+ });
- expect(this.store.environmentsData.length).toEqual(2);
+ it('removes the data if all the values from a query are not defined', () => {
+ expect(store.groups[1].metrics[0].queries[0].result.length).toEqual(0);
});
});
diff --git a/spec/javascripts/pipelines/graph/job_item_spec.js b/spec/javascripts/pipelines/graph/job_item_spec.js
index 88e1789184d..1cdb0aff524 100644
--- a/spec/javascripts/pipelines/graph/job_item_spec.js
+++ b/spec/javascripts/pipelines/graph/job_item_spec.js
@@ -139,57 +139,17 @@ describe('pipeline graph job item', () => {
});
});
- describe('tooltip placement', () => {
- it('does not set tooltip boundary by default', () => {
- component = mountComponent(JobComponent, {
- job: mockJob,
- });
-
- expect(component.tooltipBoundary).toBeNull();
- });
-
- it('sets tooltip boundary to viewport for small dropdowns', () => {
- component = mountComponent(JobComponent, {
- job: mockJob,
- dropdownLength: 1,
- });
-
- expect(component.tooltipBoundary).toEqual('viewport');
- });
-
- it('does not set tooltip boundary for large lists', () => {
- component = mountComponent(JobComponent, {
- job: mockJob,
- dropdownLength: 7,
- });
-
- expect(component.tooltipBoundary).toBeNull();
- });
- });
-
describe('for delayed job', () => {
- beforeEach(() => {
- const fifteenMinutesInMilliseconds = 900000;
- spyOn(Date, 'now').and.callFake(
- () => new Date(delayedJobFixture.scheduled_at).getTime() - fifteenMinutesInMilliseconds,
- );
- });
-
- it('displays remaining time in tooltip', done => {
+ it('displays remaining time in tooltip', () => {
component = mountComponent(JobComponent, {
job: delayedJobFixture,
});
- Vue.nextTick()
- .then(() => {
- expect(
- component.$el
- .querySelector('.js-pipeline-graph-job-link')
- .getAttribute('data-original-title'),
- ).toEqual('delayed job - delayed manual action (00:15:00)');
- })
- .then(done)
- .catch(done.fail);
+ expect(
+ component.$el
+ .querySelector('.js-pipeline-graph-job-link')
+ .getAttribute('data-original-title'),
+ ).toEqual(`delayed job - delayed manual action (${component.remainingTime})`);
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js
new file mode 100644
index 00000000000..16c8c939a6f
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js
@@ -0,0 +1,51 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MrWidgetContainer from '~/vue_merge_request_widget/components/mr_widget_container.vue';
+
+const BODY_HTML = '<div class="test-body">Hello World</div>';
+const FOOTER_HTML = '<div class="test-footer">Goodbye!</div>';
+
+describe('MrWidgetContainer', () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(localVue.extend(MrWidgetContainer), {
+ localVue,
+ ...options,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has layout', () => {
+ factory();
+
+ expect(wrapper.is('.mr-widget-heading')).toBe(true);
+ expect(wrapper.contains('.mr-widget-content')).toBe(true);
+ });
+
+ it('accepts default slot', () => {
+ factory({
+ slots: {
+ default: BODY_HTML,
+ },
+ });
+
+ expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true);
+ });
+
+ it('accepts footer slot', () => {
+ factory({
+ slots: {
+ default: BODY_HTML,
+ footer: FOOTER_HTML,
+ },
+ });
+
+ expect(wrapper.contains('.mr-widget-content .test-body')).toBe(true);
+ expect(wrapper.contains('.test-footer')).toBe(true);
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js
new file mode 100644
index 00000000000..f7c2376eebf
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js
@@ -0,0 +1,30 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+
+const TEST_ICON = 'commit';
+
+describe('MrWidgetIcon', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(localVue.extend(MrWidgetIcon), {
+ propsData: {
+ name: TEST_ICON,
+ },
+ sync: false,
+ localVue,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders icon and container', () => {
+ expect(wrapper.is('.circle-icon-container')).toBe(true);
+ expect(wrapper.find(Icon).props('name')).toEqual(TEST_ICON);
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
new file mode 100644
index 00000000000..e5155573f6f
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
@@ -0,0 +1,90 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
+import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import { mockStore } from '../mock_data';
+
+describe('MrWidgetPipelineContainer', () => {
+ let wrapper;
+
+ const factory = (props = {}) => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(localVue.extend(MrWidgetPipelineContainer), {
+ propsData: {
+ mr: Object.assign({}, mockStore),
+ ...props,
+ },
+ localVue,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when pre merge', () => {
+ beforeEach(() => {
+ factory();
+ });
+
+ it('renders pipeline', () => {
+ expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
+ expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
+ jasmine.objectContaining({
+ pipeline: mockStore.pipeline,
+ ciStatus: mockStore.ciStatus,
+ hasCi: mockStore.hasCI,
+ sourceBranch: mockStore.sourceBranch,
+ sourceBranchLink: mockStore.sourceBranchLink,
+ }),
+ );
+ });
+
+ it('renders deployments', () => {
+ const expectedProps = mockStore.deployments.map(dep =>
+ jasmine.objectContaining({
+ deployment: dep,
+ showMetrics: false,
+ }),
+ );
+
+ const deployments = wrapper.findAll('.mr-widget-extension .js-pre-deployment');
+
+ expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
+ });
+ });
+
+ describe('when post merge', () => {
+ beforeEach(() => {
+ factory({
+ isPostMerge: true,
+ });
+ });
+
+ it('renders pipeline', () => {
+ expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
+ expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
+ jasmine.objectContaining({
+ pipeline: mockStore.mergePipeline,
+ ciStatus: mockStore.ciStatus,
+ hasCi: mockStore.hasCI,
+ sourceBranch: mockStore.targetBranch,
+ sourceBranchLink: mockStore.targetBranch,
+ }),
+ );
+ });
+
+ it('renders deployments', () => {
+ const expectedProps = mockStore.postMergeDeployments.map(dep =>
+ jasmine.objectContaining({
+ deployment: dep,
+ showMetrics: true,
+ }),
+ );
+
+ const deployments = wrapper.findAll('.mr-widget-extension .js-post-deployment');
+
+ expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 17554c4fe42..072e98fc0e8 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -222,3 +222,16 @@ export default {
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help',
};
+
+export const mockStore = {
+ pipeline: { id: 0 },
+ mergePipeline: { id: 1 },
+ targetBranch: 'target-branch',
+ sourceBranch: 'source-branch',
+ sourceBranchLink: 'source-branch-link',
+ deployments: [{ id: 0, name: 'bogus' }, { id: 1, name: 'bogus-docs' }],
+ postMergeDeployments: [{ id: 0, name: 'prod' }, { id: 1, name: 'prod-docs' }],
+ troubleshootingDocsPath: 'troubleshooting-docs-path',
+ ciStatus: 'ci-status',
+ hasCI: true,
+};
diff --git a/spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js
new file mode 100644
index 00000000000..c15635f2105
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/notes/timeline_entry_item_spec.js
@@ -0,0 +1,40 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+describe(TimelineEntryItem.name, () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(TimelineEntryItem, {
+ localVue,
+ ...options,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correctly', () => {
+ factory();
+
+ expect(wrapper.is('.timeline-entry')).toBe(true);
+
+ expect(wrapper.contains('.timeline-entry-inner')).toBe(true);
+ });
+
+ it('accepts default slot', () => {
+ const dummyContent = '<p>some content</p>';
+ factory({
+ slots: {
+ default: dummyContent,
+ },
+ });
+
+ const content = wrapper.find('.timeline-entry-inner :first-child');
+
+ expect(content.html()).toBe(dummyContent);
+ });
+});
diff --git a/spec/lib/gitlab/auth/user_auth_finders_spec.rb b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
index 5d3fbba264f..4e4c8b215c2 100644
--- a/spec/lib/gitlab/auth/user_auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/user_auth_finders_spec.rb
@@ -279,5 +279,20 @@ describe Gitlab::Auth::UserAuthFinders do
expect { validate_access_token!(scopes: [:sudo]) }.to raise_error(Gitlab::Auth::InsufficientScopeError)
end
end
+
+ context 'with impersonation token' do
+ let(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
+
+ context 'when impersonation is disabled' do
+ before do
+ stub_config_setting(impersonation_enabled: false)
+ allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
+ end
+
+ it 'returns Gitlab::Auth::ImpersonationDisabled' do
+ expect { validate_access_token! }.to raise_error(Gitlab::Auth::ImpersonationDisabled)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb
index b028b771375..abb4fff3ad7 100644
--- a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb
+++ b/spec/lib/gitlab/config/entry/attributable_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Attributable do
+describe Gitlab::Config::Entry::Attributable do
let(:node) do
Class.new do
- include Gitlab::Ci::Config::Entry::Attributable
+ include Gitlab::Config::Entry::Attributable
end
end
@@ -48,7 +48,7 @@ describe Gitlab::Ci::Config::Entry::Attributable do
it 'raises an error' do
expectation = expect do
Class.new(String) do
- include Gitlab::Ci::Config::Entry::Attributable
+ include Gitlab::Config::Entry::Attributable
attributes :length
end
diff --git a/spec/lib/gitlab/ci/config/entry/boolean_spec.rb b/spec/lib/gitlab/config/entry/boolean_spec.rb
index 5f067cad93c..1b7a3f850ec 100644
--- a/spec/lib/gitlab/ci/config/entry/boolean_spec.rb
+++ b/spec/lib/gitlab/config/entry/boolean_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Boolean do
+describe Gitlab::Config::Entry::Boolean do
let(:entry) { described_class.new(config) }
describe 'validations' do
diff --git a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb b/spec/lib/gitlab/config/entry/configurable_spec.rb
index 088d4b472da..85a7cf1d241 100644
--- a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb
+++ b/spec/lib/gitlab/config/entry/configurable_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Configurable do
+describe Gitlab::Config::Entry::Configurable do
let(:entry) do
- Class.new(Gitlab::Ci::Config::Entry::Node) do
- include Gitlab::Ci::Config::Entry::Configurable
+ Class.new(Gitlab::Config::Entry::Node) do
+ include Gitlab::Config::Entry::Configurable
end
end
@@ -39,7 +39,7 @@ describe Gitlab::Ci::Config::Entry::Configurable do
it 'creates a node factory' do
expect(entry.nodes[:object])
- .to be_an_instance_of Gitlab::Ci::Config::Entry::Factory
+ .to be_an_instance_of Gitlab::Config::Entry::Factory
end
it 'returns a duplicated factory object' do
diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/config/entry/factory_spec.rb
index 8dd48e4efae..c29d17eaee3 100644
--- a/spec/lib/gitlab/ci/config/entry/factory_spec.rb
+++ b/spec/lib/gitlab/config/entry/factory_spec.rb
@@ -1,9 +1,17 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Factory do
+describe Gitlab::Config::Entry::Factory do
describe '#create!' do
+ class Script < Gitlab::Config::Entry::Node
+ include Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, array_of_strings: true
+ end
+ end
+
+ let(:entry) { Script }
let(:factory) { described_class.new(entry) }
- let(:entry) { Gitlab::Ci::Config::Entry::Script }
context 'when setting a concrete value' do
it 'creates entry with valid value' do
@@ -54,7 +62,7 @@ describe Gitlab::Ci::Config::Entry::Factory do
context 'when not setting a value' do
it 'raises error' do
expect { factory.create! }.to raise_error(
- Gitlab::Ci::Config::Entry::Factory::InvalidFactory
+ Gitlab::Config::Entry::Factory::InvalidFactory
)
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb b/spec/lib/gitlab/config/entry/simplifiable_spec.rb
index 395062207a3..bc8387ada67 100644
--- a/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb
+++ b/spec/lib/gitlab/config/entry/simplifiable_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Simplifiable do
+describe Gitlab::Config::Entry::Simplifiable do
describe '.strategy' do
let(:entry) do
Class.new(described_class) do
diff --git a/spec/lib/gitlab/ci/config/entry/undefined_spec.rb b/spec/lib/gitlab/config/entry/undefined_spec.rb
index fdf48d84192..48f9d276c95 100644
--- a/spec/lib/gitlab/ci/config/entry/undefined_spec.rb
+++ b/spec/lib/gitlab/config/entry/undefined_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Undefined do
+describe Gitlab::Config::Entry::Undefined do
let(:entry) { described_class.new }
describe '#leaf?' do
diff --git a/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb b/spec/lib/gitlab/config/entry/unspecified_spec.rb
index 66f88fa35b6..64421824a12 100644
--- a/spec/lib/gitlab/ci/config/entry/unspecified_spec.rb
+++ b/spec/lib/gitlab/config/entry/unspecified_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Unspecified do
+describe Gitlab::Config::Entry::Unspecified do
let(:unspecified) { described_class.new(entry) }
let(:entry) { spy('Entry') }
diff --git a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb b/spec/lib/gitlab/config/entry/validatable_spec.rb
index ae2a7a51ba6..5a8f9766d23 100644
--- a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb
+++ b/spec/lib/gitlab/config/entry/validatable_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Validatable do
+describe Gitlab::Config::Entry::Validatable do
let(:entry) do
- Class.new(Gitlab::Ci::Config::Entry::Node) do
- include Gitlab::Ci::Config::Entry::Validatable
+ Class.new(Gitlab::Config::Entry::Node) do
+ include Gitlab::Config::Entry::Validatable
end
end
@@ -20,7 +20,7 @@ describe Gitlab::Ci::Config::Entry::Validatable do
it 'returns validator' do
expect(entry.validator.superclass)
- .to be Gitlab::Ci::Config::Entry::Validator
+ .to be Gitlab::Config::Entry::Validator
end
it 'returns only one validator to mitigate leaks' do
diff --git a/spec/lib/gitlab/ci/config/entry/validator_spec.rb b/spec/lib/gitlab/config/entry/validator_spec.rb
index 172b6b47a4f..efa16c4265c 100644
--- a/spec/lib/gitlab/ci/config/entry/validator_spec.rb
+++ b/spec/lib/gitlab/config/entry/validator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Entry::Validator do
+describe Gitlab::Config::Entry::Validator do
let(:validator) { Class.new(described_class) }
let(:validator_instance) { validator.new(node) }
let(:node) { spy('node') }
diff --git a/spec/lib/gitlab/ci/config/loader_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb
index 590fc8904c1..44c9a3896a8 100644
--- a/spec/lib/gitlab/ci/config/loader_spec.rb
+++ b/spec/lib/gitlab/config/loader/yaml_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Loader do
+describe Gitlab::Config::Loader::Yaml do
let(:loader) { described_class.new(yml) }
context 'when yaml syntax is correct' do
@@ -31,7 +31,7 @@ describe Gitlab::Ci::Config::Loader do
describe '#load!' do
it 'raises error' do
expect { loader.load! }.to raise_error(
- Gitlab::Ci::Config::Loader::FormatError,
+ Gitlab::Config::Loader::FormatError,
'Invalid configuration format'
)
end
@@ -43,7 +43,7 @@ describe Gitlab::Ci::Config::Loader do
describe '#initialize' do
it 'raises FormatError' do
- expect { loader }.to raise_error(Gitlab::Ci::Config::Loader::FormatError, 'Unknown alias: bad_alias')
+ expect { loader }.to raise_error(Gitlab::Config::Loader::FormatError, 'Unknown alias: bad_alias')
end
end
end
diff --git a/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb b/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb
new file mode 100644
index 00000000000..f518bb3dc3e
--- /dev/null
+++ b/spec/lib/gitlab/database/count/exact_count_strategy_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::Database::Count::ExactCountStrategy do
+ before do
+ create_list(:project, 3)
+ create(:identity)
+ end
+
+ let(:models) { [Project, Identity] }
+
+ subject { described_class.new(models).count }
+
+ describe '#count' do
+ it 'counts all models' do
+ expect(models).to all(receive(:count).and_call_original)
+
+ expect(subject).to eq({ Project => 3, Identity => 1 })
+ end
+ end
+
+ describe '.enabled?' do
+ it 'is enabled for PostgreSQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(described_class.enabled?).to be_truthy
+ end
+
+ it 'is enabled for MySQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.enabled?).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
new file mode 100644
index 00000000000..b44e8c5a110
--- /dev/null
+++ b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Database::Count::ReltuplesCountStrategy do
+ before do
+ create_list(:project, 3)
+ create(:identity)
+ end
+
+ let(:models) { [Project, Identity] }
+ subject { described_class.new(models).count }
+
+ describe '#count', :postgresql do
+ context 'when reltuples is up to date' do
+ before do
+ ActiveRecord::Base.connection.execute('ANALYZE projects')
+ ActiveRecord::Base.connection.execute('ANALYZE identities')
+ end
+
+ it 'uses statistics to do the count' do
+ models.each { |model| expect(model).not_to receive(:count) }
+
+ expect(subject).to eq({ Project => 3, Identity => 1 })
+ end
+ end
+
+ context 'insufficient permissions' do
+ it 'returns an empty hash' do
+ allow(ActiveRecord::Base).to receive(:transaction).and_raise(PG::InsufficientPrivilege)
+
+ expect(subject).to eq({})
+ end
+ end
+ end
+
+ describe '.enabled?' do
+ it 'is enabled for PostgreSQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(described_class.enabled?).to be_truthy
+ end
+
+ it 'is disabled for MySQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.enabled?).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
new file mode 100644
index 00000000000..203f9344a41
--- /dev/null
+++ b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::Database::Count::TablesampleCountStrategy do
+ before do
+ create_list(:project, 3)
+ create(:identity)
+ end
+
+ let(:models) { [Project, Identity] }
+ let(:strategy) { described_class.new(models) }
+
+ subject { strategy.count }
+
+ describe '#count', :postgresql do
+ let(:estimates) { { Project => threshold + 1, Identity => threshold - 1 } }
+ let(:threshold) { Gitlab::Database::Count::TablesampleCountStrategy::EXACT_COUNT_THRESHOLD }
+
+ before do
+ allow(strategy).to receive(:size_estimates).with(check_statistics: false).and_return(estimates)
+ end
+
+ context 'for tables with an estimated small size' do
+ it 'performs an exact count' do
+ expect(Identity).to receive(:count).and_call_original
+
+ expect(subject).to include({ Identity => 1 })
+ end
+ end
+
+ context 'for tables with an estimated large size' do
+ it 'performs a tablesample count' do
+ expect(Project).not_to receive(:count)
+
+ result = subject
+ expect(result[Project]).to eq(3)
+ end
+ end
+
+ context 'insufficient permissions' do
+ it 'returns an empty hash' do
+ allow(strategy).to receive(:size_estimates).and_raise(PG::InsufficientPrivilege)
+
+ expect(subject).to eq({})
+ end
+ end
+ end
+
+ describe '.enabled?' do
+ before do
+ stub_feature_flags(tablesample_counts: true)
+ end
+
+ it 'is enabled for PostgreSQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+ expect(described_class.enabled?).to be_truthy
+ end
+
+ it 'is disabled for MySQL' do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.enabled?).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/count_spec.rb b/spec/lib/gitlab/database/count_spec.rb
index 407d9470785..1d096b8fa7c 100644
--- a/spec/lib/gitlab/database/count_spec.rb
+++ b/spec/lib/gitlab/database/count_spec.rb
@@ -8,63 +8,51 @@ describe Gitlab::Database::Count do
let(:models) { [Project, Identity] }
- describe '.approximate_counts' do
- context 'with MySQL' do
- context 'when reltuples have not been updated' do
- it 'counts all models the normal way' do
- expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ context '.approximate_counts' do
+ context 'selecting strategies' do
+ let(:strategies) { [double('s1', enabled?: true), double('s2', enabled?: false)] }
- expect(Project).to receive(:count).and_call_original
- expect(Identity).to receive(:count).and_call_original
+ it 'uses only enabled strategies' do
+ expect(strategies[0]).to receive(:new).and_return(double('strategy1', count: {}))
+ expect(strategies[1]).not_to receive(:new)
- expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
- end
+ described_class.approximate_counts(models, strategies: strategies)
end
end
- context 'with PostgreSQL', :postgresql do
- describe 'when reltuples have not been updated' do
- it 'counts all models the normal way' do
- expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({})
+ context 'fallbacks' do
+ subject { described_class.approximate_counts(models, strategies: strategies) }
- expect(Project).to receive(:count).and_call_original
- expect(Identity).to receive(:count).and_call_original
- expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
- end
+ let(:strategies) do
+ [
+ double('s1', enabled?: true, new: first_strategy),
+ double('s2', enabled?: true, new: second_strategy)
+ ]
end
- describe 'no permission' do
- it 'falls back to standard query' do
- allow(described_class).to receive(:postgresql_estimate_query).and_raise(PG::InsufficientPrivilege)
+ let(:first_strategy) { double('first strategy', count: {}) }
+ let(:second_strategy) { double('second strategy', count: {}) }
- expect(Project).to receive(:count).and_call_original
- expect(Identity).to receive(:count).and_call_original
- expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
- end
+ it 'gets results from first strategy' do
+ expect(strategies[0]).to receive(:new).with(models).and_return(first_strategy)
+ expect(first_strategy).to receive(:count)
+
+ subject
end
- describe 'when some reltuples have been updated' do
- it 'counts projects in the fast way' do
- expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({ 'projects' => 3 })
+ it 'gets more results from second strategy if some counts are missing' do
+ expect(first_strategy).to receive(:count).and_return({ Project => 3 })
+ expect(strategies[1]).to receive(:new).with([Identity]).and_return(second_strategy)
+ expect(second_strategy).to receive(:count).and_return({ Identity => 1 })
- expect(Project).not_to receive(:count).and_call_original
- expect(Identity).to receive(:count).and_call_original
- expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
- end
+ expect(subject).to eq({ Project => 3, Identity => 1 })
end
- describe 'when all reltuples have been updated' do
- before do
- ActiveRecord::Base.connection.execute('ANALYZE projects')
- ActiveRecord::Base.connection.execute('ANALYZE identities')
- end
-
- it 'counts models with the standard way' do
- expect(Project).not_to receive(:count)
- expect(Identity).not_to receive(:count)
+ it 'does not get more results as soon as all counts are present' do
+ expect(first_strategy).to receive(:count).and_return({ Project => 3, Identity => 1 })
+ expect(strategies[1]).not_to receive(:new)
- expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
- end
+ subject
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 1d184375a52..31ab11bbf8d 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -121,6 +121,8 @@ pipelines:
- artifacts
- pipeline_schedule
- merge_requests
+- deployments
+- environments
pipeline_variables:
- pipeline
stages:
@@ -245,6 +247,7 @@ project:
- protected_branches
- protected_tags
- project_members
+- project_repository
- users
- requesters
- deploy_keys_projects
diff --git a/spec/migrations/clean_up_for_members_spec.rb b/spec/migrations/clean_up_for_members_spec.rb
index 0258860d169..7876536cb3e 100644
--- a/spec/migrations/clean_up_for_members_spec.rb
+++ b/spec/migrations/clean_up_for_members_spec.rb
@@ -3,6 +3,7 @@ require Rails.root.join('db', 'migrate', '20171216111734_clean_up_for_members.rb
describe CleanUpForMembers, :migration do
let(:migration) { described_class.new }
+ let(:groups) { table(:namespaces) }
let!(:group_member) { create_group_member }
let!(:unbinded_group_member) { create_group_member }
let!(:invited_group_member) { create_group_member(true) }
@@ -25,7 +26,7 @@ describe CleanUpForMembers, :migration do
end
def create_group_member(invited = false)
- fill_member(GroupMember.new(group: create_group), invited)
+ fill_member(GroupMember.new(source_id: create_group.id, source_type: 'Namespace'), invited)
end
def create_project_member(invited = false)
@@ -54,7 +55,7 @@ describe CleanUpForMembers, :migration do
def create_group
name = FFaker::Lorem.characters(10)
- Group.create(name: name, path: name.downcase.gsub(/\s/, '_'))
+ groups.create!(type: 'Group', name: name, path: name.downcase.gsub(/\s/, '_'))
end
def create_project
diff --git a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
index 4af51217031..8c55daf0d37 100644
--- a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
+++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
@@ -94,17 +94,18 @@ describe DeleteInconsistentInternalIdRecords, :migration do
end
context 'for milestones (by group)' do
- # milestones (by group) is a little different than all of the other models
- let!(:group1) { create(:group) }
- let!(:group2) { create(:group) }
- let!(:group3) { create(:group) }
+ # milestones (by group) is a little different than most of the other models
+ let(:groups) { table(:namespaces) }
+ let(:group1) { groups.create(name: 'Group 1', type: 'Group', path: 'group_1') }
+ let(:group2) { groups.create(name: 'Group 2', type: 'Group', path: 'group_2') }
+ let(:group3) { groups.create(name: 'Group 2', type: 'Group', path: 'group_3') }
let(:internal_id_query) { ->(group) { InternalId.where(usage: InternalId.usages['milestones'], namespace: group) } }
before do
- 3.times { create(:milestone, group: group1) }
- 3.times { create(:milestone, group: group2) }
- 3.times { create(:milestone, group: group3) }
+ 3.times { create(:milestone, group_id: group1.id) }
+ 3.times { create(:milestone, group_id: group2.id) }
+ 3.times { create(:milestone, group_id: group3.id) }
internal_id_query.call(group1).first.tap do |iid|
iid.last_value = iid.last_value - 2
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 90f7e4a4590..9da16dea929 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -92,16 +92,12 @@ describe EnvironmentStatus do
end
describe '.build_environments_status' do
- subject { described_class.send(:build_environments_status, merge_request, user, sha) }
+ subject { described_class.send(:build_environments_status, merge_request, user, pipeline) }
let!(:build) { create(:ci_build, :deploy_to_production, pipeline: pipeline) }
let(:environment) { build.deployment.environment }
let(:user) { project.owner }
- before do
- build.deployment&.update!(sha: sha)
- end
-
context 'when environment is created on a forked project' do
let(:project) { create(:project, :repository) }
let(:forked) { fork_project(project, user, repository: true) }
@@ -160,6 +156,39 @@ describe EnvironmentStatus do
expect(subject.count).to eq(0)
end
end
+
+ context 'when multiple deployments with the same SHA in different environments' do
+ let(:pipeline2) { create(:ci_pipeline, sha: sha, project: project) }
+ let!(:build2) { create(:ci_build, :start_review_app, pipeline: pipeline2) }
+
+ it 'returns deployments related to the head pipeline' do
+ expect(subject.count).to eq(1)
+ expect(subject[0].environment).to eq(environment)
+ expect(subject[0].merge_request).to eq(merge_request)
+ expect(subject[0].sha).to eq(sha)
+ end
+ end
+
+ context 'when multiple deployments in the same pipeline for the same environments' do
+ let!(:build2) { create(:ci_build, :deploy_to_production, pipeline: pipeline) }
+
+ it 'returns unique entries' do
+ expect(subject.count).to eq(1)
+ expect(subject[0].environment).to eq(environment)
+ expect(subject[0].merge_request).to eq(merge_request)
+ expect(subject[0].sha).to eq(sha)
+ end
+ end
+
+ context 'when environment is stopped' do
+ before do
+ environment.stop!
+ end
+
+ it 'does not return environment status' do
+ expect(subject.count).to eq(0)
+ end
+ end
end
end
end
diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb
index 6c904710fb5..541e78507e5 100644
--- a/spec/models/pool_repository_spec.rb
+++ b/spec/models/pool_repository_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PoolRepository do
diff --git a/spec/models/project_repository_spec.rb b/spec/models/project_repository_spec.rb
new file mode 100644
index 00000000000..c966447fedc
--- /dev/null
+++ b/spec/models/project_repository_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectRepository do
+ describe 'associations' do
+ it { is_expected.to belong_to(:shard) }
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe '.find_project' do
+ it 'finds project by disk path' do
+ project = create(:project)
+ project.track_project_repository
+
+ expect(described_class.find_project(project.disk_path)).to eq(project)
+ end
+
+ it 'returns nil when it does not find the project' do
+ expect(described_class.find_project('@@unexisting/path/to/project')).to be_nil
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e98c69e636a..af5b0939ca2 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -54,6 +54,7 @@ describe Project do
it { is_expected.to have_one(:gitlab_issue_tracker_service) }
it { is_expected.to have_one(:external_wiki_service) }
it { is_expected.to have_one(:project_feature) }
+ it { is_expected.to have_one(:project_repository) }
it { is_expected.to have_one(:statistics).class_name('ProjectStatistics') }
it { is_expected.to have_one(:import_data).class_name('ProjectImportData') }
it { is_expected.to have_one(:last_event).class_name('Event') }
@@ -1618,6 +1619,30 @@ describe Project do
end
end
+ describe '#track_project_repository' do
+ let(:project) { create(:project, :repository) }
+
+ it 'creates a project_repository' do
+ project.track_project_repository
+
+ expect(project.reload.project_repository).to be_present
+ expect(project.project_repository.disk_path).to eq(project.disk_path)
+ expect(project.project_repository.shard_name).to eq(project.repository_storage)
+ end
+
+ it 'updates the project_repository' do
+ project.track_project_repository
+
+ allow(project).to receive(:disk_path).and_return('@fancy/new/path')
+
+ expect do
+ project.track_project_repository
+ end.not_to change(ProjectRepository, :count)
+
+ expect(project.reload.project_repository.disk_path).to eq(project.disk_path)
+ end
+ end
+
describe '#create_repository' do
let(:project) { create(:project, :repository) }
let(:shell) { Gitlab::Shell.new }
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index da61a5f2771..b12ca79847c 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -174,7 +174,15 @@ describe RemoteMirror do
end
context 'with remote mirroring enabled' do
+ it 'defaults to disabling only protected branches' do
+ expect(remote_mirror.only_protected_branches?).to be_falsey
+ end
+
context 'with only protected branches enabled' do
+ before do
+ remote_mirror.only_protected_branches = true
+ end
+
context 'when it did not update in the last minute' do
it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do
expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now)
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 82ff2a002e0..3682e21ca40 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -236,7 +236,8 @@ describe Todo do
create(:todo, target: create(:merge_request))
- expect(described_class.for_target(todo.target)).to eq([todo])
+ expect(described_class.for_type(Issue.name).for_target(todo.target))
+ .to contain_exactly(todo)
end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index cca449e9e56..2c40e266f5f 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -206,6 +206,19 @@ describe API::Helpers do
expect { current_user }.to raise_error Gitlab::Auth::ExpiredError
end
+
+ context 'when impersonation is disabled' do
+ let(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
+
+ before do
+ stub_config_setting(impersonation_enabled: false)
+ env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ end
+
+ it 'does not allow impersonation tokens' do
+ expect { current_user }.to raise_error Gitlab::Auth::ImpersonationDisabled
+ end
+ end
end
end
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index e2000ab42e8..145356c4df5 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Namespaces do
let(:admin) { create(:admin) }
let(:user) { create(:user) }
- let!(:group1) { create(:group) }
+ let!(:group1) { create(:group, name: 'group.one') }
let!(:group2) { create(:group, :nested) }
describe "GET /namespaces" do
diff --git a/spec/services/ci/archive_trace_service_spec.rb b/spec/services/ci/archive_trace_service_spec.rb
new file mode 100644
index 00000000000..8e9cb65f3bc
--- /dev/null
+++ b/spec/services/ci/archive_trace_service_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Ci::ArchiveTraceService, '#execute' do
+ subject { described_class.new.execute(job) }
+
+ context 'when job is finished' do
+ let(:job) { create(:ci_build, :success, :trace_live) }
+
+ it 'creates an archived trace' do
+ expect { subject }.not_to raise_error
+
+ expect(job.reload.job_artifacts_trace).to be_exist
+ end
+
+ context 'when trace is already archived' do
+ let!(:job) { create(:ci_build, :success, :trace_artifact) }
+
+ it 'ignores an exception' do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'does not create an archived trace' do
+ expect { subject }.not_to change { Ci::JobArtifact.trace.count }
+ end
+ end
+ end
+
+ context 'when job is running' do
+ let(:job) { create(:ci_build, :running, :trace_live) }
+
+ it 'increments Prometheus counter, sends crash report to Sentry and ignore an error for continuing to archive' do
+ expect(Gitlab::Sentry)
+ .to receive(:track_exception)
+ .with(::Gitlab::Ci::Trace::ArchiveError,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/51502',
+ extra: { job_id: job.id } ).once
+
+ expect(Rails.logger)
+ .to receive(:error)
+ .with("Failed to archive trace. id: #{job.id} message: Job is not finished yet")
+ .and_call_original
+
+ expect(Gitlab::Metrics)
+ .to receive(:counter)
+ .with(:job_trace_archive_failed_total, "Counter of failed attempts of trace archiving")
+ .and_call_original
+
+ expect { subject }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 4d9c5aabbda..a4582d1bc64 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -657,4 +657,37 @@ describe Ci::CreatePipelineService do
end
end
end
+
+ describe '#execute!' do
+ subject { service.execute!(*args) }
+
+ let(:service) { described_class.new(project, user, ref: ref_name) }
+ let(:args) { [:push] }
+
+ context 'when user has a permission to create a pipeline' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'does not raise an error' do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'creates a pipeline' do
+ expect { subject }.to change { Ci::Pipeline.count }.by(1)
+ end
+ end
+
+ context 'when user does not have a permission to create a pipeline' do
+ let(:user) { create(:user) }
+
+ it 'raises an error' do
+ expect { subject }
+ .to raise_error(described_class::CreateError)
+ .with_message('Insufficient permissions to create a new pipeline')
+ end
+ end
+ end
end
diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb
index 5f3c8e82715..84c48d63c64 100644
--- a/spec/services/files/multi_service_spec.rb
+++ b/spec/services/files/multi_service_spec.rb
@@ -122,26 +122,47 @@ describe Files::MultiService do
let(:action) { 'move' }
let(:new_file_path) { 'files/ruby/new_popen.rb' }
+ let(:result) { subject.execute }
+ let(:blob) { repository.blob_at_branch(branch_name, new_file_path) }
+
context 'when original file has been updated' do
before do
update_file(original_file_path)
end
it 'rejects the commit' do
- results = subject.execute
-
- expect(results[:status]).to eq(:error)
- expect(results[:message]).to match(original_file_path)
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to match(original_file_path)
end
end
- context 'when original file have not been updated' do
+ context 'when original file has not been updated' do
it 'moves the file' do
- results = subject.execute
- blob = project.repository.blob_at_branch(branch_name, new_file_path)
-
- expect(results[:status]).to eq(:success)
+ expect(result[:status]).to eq(:success)
expect(blob).to be_present
+ expect(blob.data).to eq(file_content)
+ end
+
+ context 'when content is nil' do
+ let(:file_content) { nil }
+
+ it 'moves the existing content untouched' do
+ original_content = repository.blob_at_branch(branch_name, original_file_path).data
+
+ expect(result[:status]).to eq(:success)
+ expect(blob).to be_present
+ expect(blob.data).to eq(original_content)
+ end
+ end
+
+ context 'when content is an empty string' do
+ let(:file_content) { '' }
+
+ it 'moves the file and empties it' do
+ expect(result[:status]).to eq(:success)
+ expect(blob).not_to be_nil
+ expect(blob.data).to eq('')
+ end
end
end
end
diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb
index ad0053ec5cf..a1ae428586e 100644
--- a/spec/support/helpers/features/sorting_helpers.rb
+++ b/spec/support/helpers/features/sorting_helpers.rb
@@ -13,7 +13,7 @@ module Spec
module Features
module SortingHelpers
def sort_by(value)
- find('button.dropdown-toggle').click
+ find('.filter-dropdown-container button.dropdown-menu-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link(value)
diff --git a/spec/support/helpers/sorting_helper.rb b/spec/support/helpers/sorting_helper.rb
index 9496a94d8f4..e505a6b7258 100644
--- a/spec/support/helpers/sorting_helper.rb
+++ b/spec/support/helpers/sorting_helper.rb
@@ -10,7 +10,7 @@
#
module SortingHelper
def sorting_by(value)
- find('button.dropdown-toggle').click
+ find('.filter-dropdown-container button.dropdown-menu-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-right li') do
click_link value
end
diff --git a/spec/workers/archive_trace_worker_spec.rb b/spec/workers/archive_trace_worker_spec.rb
index b768588c6e1..7244ad4f199 100644
--- a/spec/workers/archive_trace_worker_spec.rb
+++ b/spec/workers/archive_trace_worker_spec.rb
@@ -5,10 +5,11 @@ describe ArchiveTraceWorker do
subject { described_class.new.perform(job&.id) }
context 'when job is found' do
- let(:job) { create(:ci_build) }
+ let(:job) { create(:ci_build, :trace_live) }
it 'executes service' do
- expect_any_instance_of(Gitlab::Ci::Trace).to receive(:archive!)
+ expect_any_instance_of(Ci::ArchiveTraceService)
+ .to receive(:execute).with(job)
subject
end
@@ -18,7 +19,8 @@ describe ArchiveTraceWorker do
let(:job) { nil }
it 'does not execute service' do
- expect_any_instance_of(Gitlab::Ci::Trace).not_to receive(:archive!)
+ expect_any_instance_of(Ci::ArchiveTraceService)
+ .not_to receive(:execute)
subject
end
diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb
index 23f5dda298a..478fb7d2c0f 100644
--- a/spec/workers/ci/archive_traces_cron_worker_spec.rb
+++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb
@@ -30,6 +30,13 @@ describe Ci::ArchiveTracesCronWorker do
it_behaves_like 'archives trace'
+ it 'executes service' do
+ expect_any_instance_of(Ci::ArchiveTraceService)
+ .to receive(:execute).with(build)
+
+ subject
+ end
+
context 'when a trace had already been archived' do
let!(:build) { create(:ci_build, :success, :trace_live, :trace_artifact) }
let!(:build2) { create(:ci_build, :success, :trace_live) }
@@ -46,11 +53,12 @@ describe Ci::ArchiveTracesCronWorker do
let!(:build) { create(:ci_build, :success, :trace_live) }
before do
+ allow(Gitlab::Sentry).to receive(:track_exception)
allow_any_instance_of(Gitlab::Ci::Trace).to receive(:archive!).and_raise('Unexpected error')
end
it 'puts a log' do
- expect(Rails.logger).to receive(:error).with("Failed to archive stale live trace. id: #{build.id} message: Unexpected error")
+ expect(Rails.logger).to receive(:error).with("Failed to archive trace. id: #{build.id} message: Unexpected error")
subject
end
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index a2fe4734d47..c5a60e9855b 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -56,17 +56,89 @@ describe PipelineScheduleWorker do
expect { subject }.not_to change { project.pipelines.count }
end
end
+
+ context 'when gitlab-ci.yml is corrupted' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } ))
+ end
+
+ it 'creates a failed pipeline with the reason' do
+ expect { subject }.to change { project.pipelines.count }.by(1)
+ expect(Ci::Pipeline.last).to be_config_error
+ expect(Ci::Pipeline.last.yaml_errors).not_to be_nil
+ end
+ end
end
context 'when the schedule is not runnable by the user' do
- it 'deactivates the schedule' do
+ before do
+ expect(Gitlab::Sentry)
+ .to receive(:track_exception)
+ .with(Ci::CreatePipelineService::CreateError,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: pipeline_schedule.id } ).once
+ end
+
+ it 'does not deactivate the schedule' do
+ subject
+
+ expect(pipeline_schedule.reload.active).to be_truthy
+ end
+
+ it 'increments Prometheus counter' do
+ expect(Gitlab::Metrics)
+ .to receive(:counter)
+ .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation")
+ .and_call_original
+
+ subject
+ end
+
+ it 'logging a pipeline error' do
+ expect(Rails.logger)
+ .to receive(:error)
+ .with(a_string_matching("Insufficient permissions to create a new pipeline"))
+ .and_call_original
+
subject
+ end
+
+ it 'does not create a pipeline' do
+ expect { subject }.not_to change { project.pipelines.count }
+ end
- expect(pipeline_schedule.reload.active).to be_falsy
+ it 'does not raise an exception' do
+ expect { subject }.not_to raise_error
end
+ end
+
+ context 'when .gitlab-ci.yml is missing in the project' do
+ before do
+ stub_ci_pipeline_yaml_file(nil)
+ project.add_maintainer(user)
- it 'does not schedule a pipeline' do
+ expect(Gitlab::Sentry)
+ .to receive(:track_exception)
+ .with(Ci::CreatePipelineService::CreateError,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: pipeline_schedule.id } ).once
+ end
+
+ it 'logging a pipeline error' do
+ expect(Rails.logger)
+ .to receive(:error)
+ .with(a_string_matching("Missing .gitlab-ci.yml file"))
+ .and_call_original
+
+ subject
+ end
+
+ it 'does not create a pipeline' do
expect { subject }.not_to change { project.pipelines.count }
end
+
+ it 'does not raise an exception' do
+ expect { subject }.not_to raise_error
+ end
end
end
diff --git a/vendor/jupyter/values.yaml b/vendor/jupyter/values.yaml
index 24136a7aca5..781d6e3042f 100644
--- a/vendor/jupyter/values.yaml
+++ b/vendor/jupyter/values.yaml
@@ -22,3 +22,4 @@ ingress:
enabled: true
annotations:
kubernetes.io/ingress.class: "nginx"
+ kubernetes.io/tls-acme: "true"
diff --git a/yarn.lock b/yarn.lock
index 6d5ff62715b..623e38815aa 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1933,10 +1933,10 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
-codesandbox-api@^0.0.18:
- version "0.0.18"
- resolved "https://registry.yarnpkg.com/codesandbox-api/-/codesandbox-api-0.0.18.tgz#56b96b37533f80d20c21861e5e477d3557e613ca"
- integrity sha512-DHLR8QQpMplNDF9GDbV8EevwmF9mlMBQwiWB8bZBnP2NQQbklthqjpBwNjah8qlDgfD7vQNNcwT8uIZ24WZb7Q==
+codesandbox-api@^0.0.20:
+ version "0.0.20"
+ resolved "https://registry.yarnpkg.com/codesandbox-api/-/codesandbox-api-0.0.20.tgz#174bcd76c9f31521175c6bceabc37da6b1fbc30b"
+ integrity sha512-jhxZzAmjCKBZad8QWMeueiQVFE87igK6F2DBOEVFFJO6jgTXT8qjuzGYepr+B8bjgo/icN7bc/2xmEMBA63s2w==
codesandbox-import-util-types@^1.2.11:
version "1.2.11"
@@ -7261,12 +7261,12 @@ slugify@^1.3.1:
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.3.1.tgz#f572127e8535329fbc6c1edb74ab856b61ad7de2"
integrity sha512-6BwyhjF5tG5P8s+0DPNyJmBSBePG6iMyhjvIW5zGdA3tFik9PtK+yNkZgTeiroCRGZYgkHftFA62tGVK1EI9Kw==
-smooshpack@^0.0.48:
- version "0.0.48"
- resolved "https://registry.yarnpkg.com/smooshpack/-/smooshpack-0.0.48.tgz#6fbeaaf59226a1fe500f56aa17185eed377d2823"
- integrity sha512-BmaIr6fK6w7WBCI4V7tcZIg78WeE6X56zrhGSNk5vXavT1bQPXH+brZFq6Hwi833upY/yusG2FMVkf7TZsVv/w==
+smooshpack@^0.0.53:
+ version "0.0.53"
+ resolved "https://registry.yarnpkg.com/smooshpack/-/smooshpack-0.0.53.tgz#aa397ca43619912e8ac0aa32012846ff4feaa5e8"
+ integrity sha512-FVXvKvZOz5+Tk/zUJ/wxM+ftu1yZtFEmeQl4chCqbzK/reU0L/OdDiYpx+/27Jt2VX09j08oIzwoyQ5fHH4+WQ==
dependencies:
- codesandbox-api "^0.0.18"
+ codesandbox-api "^0.0.20"
codesandbox-import-utils "^1.2.3"
lodash.isequal "^4.5.0"