summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml10
-rw-r--r--.gitlab/issue_templates/Feature proposal.md28
-rw-r--r--.prettierrc10
-rw-r--r--.rubocop.yml6
-rw-r--r--CHANGELOG.md15
-rw-r--r--Dangerfile1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock22
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue22
-rw-r--r--app/assets/javascripts/commons/jquery.js4
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue12
-rw-r--r--app/assets/javascripts/diffs/store/actions.js3
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js12
-rw-r--r--app/assets/javascripts/emoji/index.js15
-rw-r--r--app/assets/javascripts/environments/components/container.vue20
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue12
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue14
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue43
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js3
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue11
-rw-r--r--app/assets/javascripts/environments/index.js3
-rw-r--r--app/assets/javascripts/environments/mixins/canary_callout_mixin.js5
-rw-r--r--app/assets/javascripts/environments/mixins/container_mixin.js29
-rw-r--r--app/assets/javascripts/environments/mixins/environment_item_mixin.js13
-rw-r--r--app/assets/javascripts/environments/mixins/environments_app_mixin.js32
-rw-r--r--app/assets/javascripts/environments/mixins/environments_folder_view_mixin.js29
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js2
-rw-r--r--app/assets/javascripts/environments/mixins/environments_table_mixin.js10
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js25
-rw-r--r--app/assets/javascripts/environments/stores/helpers.js8
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js12
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js31
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js13
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js12
-rw-r--r--app/assets/javascripts/lib/utils/simple_poll.js4
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js2
-rw-r--r--app/assets/javascripts/notes.js4
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue40
-rw-r--r--app/assets/javascripts/pages/groups/details/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js31
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_tabs.js (renamed from app/assets/javascripts/pages/groups/show/group_tabs.js)0
-rw-r--r--app/assets/javascripts/pages/groups/show/index.js27
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue5
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js6
-rw-r--r--app/assets/javascripts/pipelines/mixins/stage_column_mixin.js7
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js8
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js16
-rw-r--r--app/assets/javascripts/pipelines/services/pipeline_service.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue87
-rw-r--r--app/assets/stylesheets/application.scss2
-rw-r--r--app/assets/stylesheets/components/dashboard_skeleton.scss80
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/filters.scss11
-rw-r--r--app/assets/stylesheets/framework/variables_overrides.scss5
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss10
-rw-r--r--app/assets/stylesheets/pages/notes.scss8
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/assets/stylesheets/pages/settings.scss5
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss2
-rw-r--r--app/controllers/autocomplete_controller.rb9
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb18
-rw-r--r--app/controllers/groups_controller.rb36
-rw-r--r--app/finders/merge_requests_finder.rb2
-rw-r--r--app/helpers/auto_devops_helper.rb13
-rw-r--r--app/helpers/groups_helper.rb1
-rw-r--r--app/models/ci/pipeline.rb29
-rw-r--r--app/models/label.rb7
-rw-r--r--app/models/merge_request.rb33
-rw-r--r--app/models/merge_request_diff.rb10
-rw-r--r--app/models/merge_request_diff_file.rb2
-rw-r--r--app/models/namespace.rb17
-rw-r--r--app/models/note.rb8
-rw-r--r--app/models/project.rb23
-rw-r--r--app/models/project_services/jira_service.rb10
-rw-r--r--app/models/repository.rb19
-rw-r--r--app/models/user_interacted_project.rb18
-rw-r--r--app/policies/group_policy.rb3
-rw-r--r--app/presenters/project_presenter.rb10
-rw-r--r--app/serializers/detailed_status_entity.rb16
-rw-r--r--app/serializers/pipeline_entity.rb3
-rw-r--r--app/services/groups/auto_devops_service.rb17
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_link_list_service.rb16
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb20
-rw-r--r--app/validators/sha_validator.rb2
-rw-r--r--app/views/groups/_home_panel.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/_auto_devops_form.html.haml15
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml14
-rw-r--r--app/views/layouts/header/_new_dropdown.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml11
-rw-r--r--app/views/projects/_flash_messages.html.haml3
-rw-r--r--app/views/projects/_home_panel.html.haml5
-rw-r--r--app/views/projects/blob/_header_content.html.haml2
-rw-r--r--app/views/projects/empty.html.haml133
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml8
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/settings/operations/_error_tracking.html.haml2
-rw-r--r--app/views/projects/settings/operations/show.html.haml1
-rw-r--r--app/views/shared/_file_highlight.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/views/shared/notes/_note.html.haml15
-rw-r--r--app/views/shared/snippets/_form.html.haml6
-rw-r--r--app/views/shared/snippets/_header.html.haml8
-rw-r--r--app/workers/concerns/waitable_worker.rb8
-rw-r--r--app/workers/create_gpg_signature_worker.rb8
-rw-r--r--app/workers/emails_on_push_worker.rb34
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb8
-rw-r--r--app/workers/pipeline_schedule_worker.rb19
-rw-r--r--app/workers/remove_expired_members_worker.rb8
-rw-r--r--changelogs/unreleased/10029-env-item.yml5
-rw-r--r--changelogs/unreleased/10081-env-table.yml5
-rw-r--r--changelogs/unreleased/25942-remove-fake-repository-path-response.yml5
-rw-r--r--changelogs/unreleased/48324-enable-squash-message-on-fast-forward.yml5
-rw-r--r--changelogs/unreleased/49863-ingress-ip-loading-state.yml5
-rw-r--r--changelogs/unreleased/52447-auto-devops-at-group-level.yml5
-rw-r--r--changelogs/unreleased/56833-project-improve-empty-repository-state-ui-fe.yml5
-rw-r--r--changelogs/unreleased/56970-fix-mr-stuck-loading-on-error.yml5
-rw-r--r--changelogs/unreleased/57330-fix-comment-edited.yml5
-rw-r--r--changelogs/unreleased/57579-gitlab-project-import-fails-sidekiq-undefined-method-import_jid.yml5
-rw-r--r--changelogs/unreleased/58149-fix-read-list-board-policy.yml6
-rw-r--r--changelogs/unreleased/58208-explicitly-set-masterauth.yml6
-rw-r--r--changelogs/unreleased/58739-hashed-storage-prevent-a-migration-and-rollback-running-at-the-same-time.yml5
-rw-r--r--changelogs/unreleased/58781-silent-progress-in-auto-devops.yml5
-rw-r--r--changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml5
-rw-r--r--changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml5
-rw-r--r--changelogs/unreleased/allow-filtering-labels-by-a-single-character.yml5
-rw-r--r--changelogs/unreleased/expose-group-id-on-home-panel.yml5
-rw-r--r--changelogs/unreleased/filter-merge-requests-by-target-branch.yml5
-rw-r--r--changelogs/unreleased/fix-ide-web-worker-relative-url.yml5
-rw-r--r--changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml5
-rw-r--r--changelogs/unreleased/fix-pipeline-entity.yml5
-rw-r--r--changelogs/unreleased/fix-review-app-env-url.yml5
-rw-r--r--changelogs/unreleased/fj-58804-fix-bitbucket-import.yml5
-rw-r--r--changelogs/unreleased/jc-fix-set-project-writable.yml5
-rw-r--r--changelogs/unreleased/more-pgroup-fix.yml5
-rw-r--r--changelogs/unreleased/pravi-gitlab-ce-update-recaptcha.yml5
-rw-r--r--changelogs/unreleased/security-shared-project-private-group.yml5
-rw-r--r--changelogs/unreleased/sh-cache-root-ref-asymetrically.yml5
-rw-r--r--changelogs/unreleased/sh-fix-blank-codeowners-ce.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-58103.yml5
-rw-r--r--changelogs/unreleased/sh-revert-rack-request-health-checks.yml5
-rw-r--r--changelogs/unreleased/sh-rugged-commit-tree-entry.yml5
-rw-r--r--changelogs/unreleased/sh-skip-sti-tables-reltuples.yml5
-rw-r--r--changelogs/unreleased/update-rack-oauth2.yml5
-rw-r--r--changelogs/unreleased/use-only-all-pipelines.yml5
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/rspec_profiling.rb50
-rw-r--r--config/initializers/trusted_proxies.rb6
-rw-r--r--config/karma.config.js66
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/group.rb2
-rw-r--r--config/webpack.config.js62
-rw-r--r--danger/documentation/Dangerfile24
-rw-r--r--danger/roulette/Dangerfile4
-rw-r--r--danger/single_codebase/Dangerfile56
-rw-r--r--db/fixtures/development/02_settings.rb (renamed from db/fixtures/development/03_settings.rb)0
-rw-r--r--db/fixtures/development/03_project.rb (renamed from db/fixtures/development/04_project.rb)2
-rw-r--r--db/fixtures/development/04_labels.rb49
-rw-r--r--db/fixtures/development/09_issues.rb6
-rw-r--r--db/fixtures/development/10_merge_requests.rb6
-rw-r--r--db/fixtures/development/22_labeled_issues_seed.rb103
-rw-r--r--db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb9
-rw-r--r--db/migrate/20190225152525_add_auto_dev_ops_enabled_to_namespaces.rb9
-rw-r--r--db/schema.rb1
-rw-r--r--doc/administration/build_artifacts.md4
-rw-r--r--doc/administration/git_protocol.md11
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/administration/monitoring/performance/introduction.md4
-rw-r--r--doc/administration/monitoring/performance/prometheus.md4
-rw-r--r--doc/administration/operations.md4
-rw-r--r--doc/administration/operations/speed_up_ssh.md4
-rw-r--r--doc/administration/raketasks/storage.md48
-rw-r--r--doc/administration/repository_storage_types.md111
-rw-r--r--doc/administration/repository_storages.md4
-rw-r--r--doc/administration/user_settings.md35
-rw-r--r--doc/api/namespaces.md12
-rw-r--r--doc/api/pipeline_schedules.md6
-rw-r--r--doc/api/search.md2
-rw-r--r--doc/ci/README.md40
-rw-r--r--doc/ci/autodeploy/index.md4
-rw-r--r--doc/ci/autodeploy/quick_start_guide.md4
-rw-r--r--doc/ci/build_artifacts/README.md4
-rw-r--r--doc/ci/chatops/README.md2
-rw-r--r--doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md3
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md9
-rw-r--r--doc/ci/examples/sast_docker.md6
-rw-r--r--doc/ci/examples/test-scala-application.md9
-rw-r--r--doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md4
-rw-r--r--doc/ci/img/pipelines-goal.pngbin15284 -> 0 bytes
-rw-r--r--doc/ci/img/types-of-pipelines.pngbin12268 -> 0 bytes
-rw-r--r--doc/ci/merge_request_pipelines/index.md46
-rw-r--r--doc/ci/permissions/README.md6
-rw-r--r--doc/ci/pipelines.md410
-rw-r--r--doc/ci/review_apps/index.md2
-rw-r--r--doc/ci/services/README.md2
-rw-r--r--doc/ci/services/postgres.md2
-rw-r--r--doc/ci/triggers/README.md13
-rw-r--r--doc/ci/variables/README.md6
-rw-r--r--doc/ci/variables/where_variables_can_be_used.md4
-rw-r--r--doc/ci/yaml/README.md20
-rw-r--r--doc/container_registry/README.md4
-rw-r--r--doc/container_registry/troubleshooting.md4
-rw-r--r--doc/development/changelog.md2
-rw-r--r--doc/development/contributing/index.md1
-rw-r--r--doc/development/contributing/merge_request_workflow.md326
-rw-r--r--doc/development/documentation/styleguide.md7
-rw-r--r--doc/development/fe_guide/vuex.md4
-rw-r--r--doc/development/new_fe_guide/style/javascript.md312
-rw-r--r--doc/development/testing_guide/end_to_end_tests.md9
-rw-r--r--doc/development/testing_guide/frontend_testing.md32
-rw-r--r--doc/gitlab-basics/create-your-ssh-keys.md39
-rw-r--r--doc/gitlab-basics/img/profile_settings.pngbin2842 -> 0 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys.pngbin16531 -> 0 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.pngbin13436 -> 0 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_title.pngbin1867 -> 0 bytes
-rw-r--r--doc/hooks/custom_hooks.md4
-rw-r--r--doc/incoming_email/README.md4
-rw-r--r--doc/incoming_email/postfix.md4
-rw-r--r--doc/install/requirements.md3
-rw-r--r--doc/integration/chat_commands.md4
-rw-r--r--doc/integration/crowd.md4
-rw-r--r--doc/integration/jira.md4
-rw-r--r--doc/integration/ldap.md4
-rw-r--r--doc/integration/slack.md4
-rw-r--r--doc/logs/logs.md4
-rw-r--r--doc/monitoring/health_check.md4
-rw-r--r--doc/monitoring/performance/gitlab_configuration.md4
-rw-r--r--doc/monitoring/performance/grafana_configuration.md4
-rw-r--r--doc/monitoring/performance/influxdb_configuration.md4
-rw-r--r--doc/monitoring/performance/influxdb_schema.md4
-rw-r--r--doc/monitoring/performance/introduction.md4
-rw-r--r--doc/permissions/permissions.md4
-rw-r--r--doc/profile/README.md4
-rw-r--r--doc/profile/preferences.md4
-rw-r--r--doc/profile/two_factor_authentication.md4
-rw-r--r--doc/raketasks/backup_restore.md7
-rw-r--r--doc/topics/authentication/index.md2
-rw-r--r--doc/topics/autodevops/index.md43
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md4
-rw-r--r--doc/user/gitlab_com/index.md2
-rw-r--r--doc/user/group/clusters/index.md9
-rw-r--r--doc/user/group/index.md19
-rw-r--r--doc/user/instance_statistics/convdev.md2
-rw-r--r--doc/user/permissions.md6
-rw-r--r--doc/user/profile/img/personal_access_tokens.pngbin18553 -> 0 bytes
-rw-r--r--doc/user/profile/personal_access_tokens.md18
-rw-r--r--doc/user/project/badges.md6
-rw-r--r--doc/user/project/clusters/index.md19
-rw-r--r--doc/user/project/integrations/prometheus.md2
-rw-r--r--doc/user/project/integrations/webhooks.md21
-rw-r--r--doc/user/project/merge_requests/merge_when_pipeline_succeeds.md2
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md5
-rw-r--r--doc/user/project/pages/introduction.md2
-rw-r--r--doc/user/project/pipelines/job_artifacts.md2
-rw-r--r--doc/user/project/protected_branches.md2
-rw-r--r--doc/web_hooks/web_hooks.md4
-rw-r--r--lib/api/discussions.rb4
-rw-r--r--lib/api/groups.rb20
-rw-r--r--lib/api/helpers.rb11
-rw-r--r--lib/api/helpers/discussions_helpers.rb13
-rw-r--r--lib/api/helpers/notes_helpers.rb6
-rw-r--r--lib/api/helpers/resource_label_events_helpers.rb13
-rw-r--r--lib/api/helpers/search_helpers.rb22
-rw-r--r--lib/api/helpers/services_helpers.rb721
-rw-r--r--lib/api/internal.rb15
-rw-r--r--lib/api/notes.rb4
-rw-r--r--lib/api/project_milestones.rb16
-rw-r--r--lib/api/repositories.rb18
-rw-r--r--lib/api/resource_label_events.rb4
-rw-r--r--lib/api/search.rb25
-rw-r--r--lib/api/services.rb698
-rw-r--r--lib/api/settings.rb7
-rw-r--r--lib/api/triggers.rb2
-rw-r--r--lib/api/variables.rb10
-rw-r--r--lib/gitlab/background_migration/archive_legacy_traces.rb9
-rw-r--r--lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb12
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads.rb14
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb142
-rw-r--r--lib/gitlab/bitbucket_server_import/importer.rb40
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb2
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml10
-rw-r--r--lib/gitlab/danger/teammate.rb8
-rw-r--r--lib/gitlab/database.rb6
-rw-r--r--lib/gitlab/database/count/reltuples_count_strategy.rb18
-rw-r--r--lib/gitlab/database/count/tablesample_count_strategy.rb11
-rw-r--r--lib/gitlab/database/multi_threaded_migration.rb10
-rw-r--r--lib/gitlab/database/sha_attribute.rb2
-rw-r--r--lib/gitlab/git/commit.rb5
-rw-r--r--lib/gitlab/git/rugged_impl/commit.rb24
-rw-r--r--lib/gitlab/git/rugged_impl/repository.rb2
-rw-r--r--lib/gitlab/gitaly_client.rb28
-rw-r--r--lib/gitlab/gitaly_client/blobs_stitcher.rb20
-rw-r--r--lib/gitlab/hashed_storage/migrator.rb18
-rw-r--r--lib/gitlab/legacy_github_import/importer.rb56
-rw-r--r--lib/gitlab/metrics/influx_db.rb6
-rw-r--r--lib/gitlab/middleware/basic_health_check.rb8
-rw-r--r--lib/gitlab/recaptcha.rb4
-rw-r--r--lib/gitlab/request_context.rb8
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb2
-rw-r--r--lib/gitlab/sql/pattern.rb6
-rw-r--r--lib/gitlab/user_extractor.rb1
-rw-r--r--lib/google_api/cloud_platform/client.rb7
-rw-r--r--lib/tasks/gitlab/artifacts/migrate.rake13
-rw-r--r--lib/tasks/gitlab/cleanup.rake2
-rw-r--r--lib/tasks/gitlab/lfs/migrate.rake11
-rw-r--r--lib/tasks/gitlab/storage.rake15
-rw-r--r--lib/tasks/gitlab/traces.rake11
-rw-r--r--lib/tasks/lint.rake8
-rw-r--r--lib/tasks/migrate/migrate_iids.rake42
-rw-r--r--locale/gitlab.pot75
-rw-r--r--locale/uk/gitlab.po2252
-rw-r--r--package.json6
-rw-r--r--qa/qa.rb11
-rw-r--r--qa/qa/page/component/select2.rb2
-rw-r--r--qa/qa/page/dashboard/projects.rb2
-rw-r--r--qa/qa/page/dashboard/snippet/index.rb21
-rw-r--r--qa/qa/page/dashboard/snippet/new.rb53
-rw-r--r--qa/qa/page/dashboard/snippet/show.rb63
-rw-r--r--qa/qa/page/main/menu.rb5
-rw-r--r--qa/qa/page/project/new.rb7
-rw-r--r--qa/qa/page/project/web_ide/edit.rb8
-rw-r--r--qa/qa/resource/snippet.rb30
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb31
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb90
-rw-r--r--qa/qa/specs/helpers/quarantine.rb68
-rw-r--r--qa/spec/spec_helper.rb47
-rw-r--r--qa/spec/spec_helper_spec.rb355
-rw-r--r--qa/spec/specs/helpers/quarantine_spec.rb271
-rwxr-xr-xscripts/build_assets_image6
-rw-r--r--scripts/frontend/postinstall.js2
-rw-r--r--scripts/frontend/prettier.js4
-rw-r--r--scripts/gitaly_test.rb16
-rwxr-xr-xscripts/insert-rspec-profiling-data47
-rwxr-xr-xscripts/merge-simplecov8
-rwxr-xr-xscripts/trigger-build2
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb31
-rw-r--r--spec/controllers/groups/settings/ci_cd_controller_spec.rb73
-rw-r--r--spec/controllers/groups_controller_spec.rb37
-rw-r--r--spec/factories/ci/pipelines.rb6
-rw-r--r--spec/factories/groups.rb8
-rw-r--r--spec/factories/merge_requests.rb25
-rw-r--r--spec/factories/projects.rb4
-rw-r--r--spec/features/groups/settings/ci_cd_spec.rb45
-rw-r--r--spec/features/merge_request/user_views_diffs_spec.rb10
-rw-r--r--spec/features/merge_requests/user_filters_by_target_branch_spec.rb45
-rw-r--r--spec/features/projects/clusters/applications_spec.rb2
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb31
-rw-r--r--spec/features/projects/show/user_sees_git_instructions_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb58
-rw-r--r--spec/features/security/group/private_access_spec.rb15
-rw-r--r--spec/finders/labels_finder_spec.rb6
-rw-r--r--spec/finders/merge_requests_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/board.json3
-rw-r--r--spec/fixtures/api/schemas/entities/issue.json3
-rw-r--r--spec/fixtures/api/schemas/entities/issue_boards.json3
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_widget.json4
-rw-r--r--spec/fixtures/api/schemas/issue.json5
-rw-r--r--spec/fixtures/api/schemas/issues.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_request.json124
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json122
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js4
-rw-r--r--spec/frontend/helpers/timeout.js24
-rw-r--r--spec/frontend/test_setup.js18
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb138
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js10
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js25
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js14
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js18
-rw-r--r--spec/javascripts/environments/environment_item_spec.js18
-rw-r--r--spec/javascripts/environments/environment_table_spec.js14
-rw-r--r--spec/javascripts/environments/environments_app_spec.js5
-rw-r--r--spec/javascripts/environments/environments_store_spec.js74
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js14
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js4
-rw-r--r--spec/javascripts/fixtures/emojis.rb2
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js2
-rw-r--r--spec/javascripts/test_bundle.js72
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js26
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js141
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb6
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb20
-rw-r--r--spec/lib/gitlab/danger/teammate_spec.rb39
-rw-r--r--spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb19
-rw-r--r--spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb16
-rw-r--r--spec/lib/gitlab/database_spec.rb38
-rw-r--r--spec/lib/gitlab/git/repository_cleaner_spec.rb12
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb38
-rw-r--r--spec/lib/gitlab/hashed_storage/migrator_spec.rb52
-rw-r--r--spec/lib/gitlab/middleware/basic_health_check_spec.rb29
-rw-r--r--spec/lib/gitlab/request_context_spec.rb27
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb2
-rw-r--r--spec/lib/gitlab/user_extractor_spec.rb8
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb12
-rw-r--r--spec/migrations/add_foreign_keys_to_todos_spec.rb2
-rw-r--r--spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb24
-rw-r--r--spec/migrations/calculate_conv_dev_index_percentages_spec.rb22
-rw-r--r--spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb19
-rw-r--r--spec/migrations/delete_inconsistent_internal_id_records_spec.rb83
-rw-r--r--spec/migrations/issues_moved_to_id_foreign_key_spec.rb15
-rw-r--r--spec/migrations/migrate_old_artifacts_spec.rb32
-rw-r--r--spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb6
-rw-r--r--spec/migrations/migrate_user_project_view_spec.rb6
-rw-r--r--spec/migrations/move_personal_snippets_files_spec.rb35
-rw-r--r--spec/models/ci/build_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb170
-rw-r--r--spec/models/commit_spec.rb16
-rw-r--r--spec/models/concerns/has_ref_spec.rb4
-rw-r--r--spec/models/group_spec.rb121
-rw-r--r--spec/models/merge_request_spec.rb85
-rw-r--r--spec/models/namespace_spec.rb24
-rw-r--r--spec/models/note_spec.rb18
-rw-r--r--spec/models/project_spec.rb167
-rw-r--r--spec/models/repository_spec.rb60
-rw-r--r--spec/policies/group_policy_spec.rb13
-rw-r--r--spec/rack_servers/puma_spec.rb8
-rw-r--r--spec/requests/api/internal_spec.rb27
-rw-r--r--spec/requests/api/merge_requests_spec.rb407
-rw-r--r--spec/routing/group_routing_spec.rb4
-rw-r--r--spec/serializers/pipeline_entity_spec.rb21
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb10
-rw-r--r--spec/services/groups/auto_devops_service_spec.rb62
-rw-r--r--spec/services/projects/destroy_service_spec.rb6
-rw-r--r--spec/support/api/schema_matcher.rb24
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb2
-rw-r--r--spec/support/helpers/graphql_helpers.rb10
-rw-r--r--spec/support/helpers/stub_object_storage.rb12
-rw-r--r--spec/support/helpers/test_env.rb10
-rw-r--r--spec/support/pg_stat_activity.rb19
-rw-r--r--spec/support/shared_examples/requests/api/merge_requests_list.rb366
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb108
-rw-r--r--spec/validators/sha_validator_spec.rb9
-rw-r--r--spec/views/groups/_home_panel.html.haml_spec.rb15
-rw-r--r--spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb1
-rw-r--r--spec/views/projects/settings/operations/show.html.haml_spec.rb1
-rw-r--r--vendor/assets/javascripts/jquery.atwho.js1202
-rw-r--r--vendor/assets/javascripts/jquery.caret.js436
-rw-r--r--vendor/licenses.csv1
-rw-r--r--yarn.lock28
449 files changed, 8618 insertions, 6431 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bd29a266ccd..f453fcf9f22 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -66,6 +66,7 @@ stages:
paths:
- knapsack/
- rspec_flaky/
+ - rspec_profiling/
.use-pg: &use-pg
services:
@@ -159,6 +160,7 @@ stages:
- coverage/
- knapsack/
- rspec_flaky/
+ - rspec_profiling/
- tmp/capybara/
reports:
junit: junit_rspec.xml
@@ -336,6 +338,7 @@ retrieve-tests-metadata:
- wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
- '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
- mkdir -p rspec_flaky/
+ - mkdir -p rspec_profiling/
- wget -O $FLAKY_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$FLAKY_RSPEC_SUITE_REPORT_PATH || rm $FLAKY_RSPEC_SUITE_REPORT_PATH
- '[[ -f $FLAKY_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${FLAKY_RSPEC_SUITE_REPORT_PATH}'
@@ -350,7 +353,7 @@ update-tests-metadata:
- rspec_flaky/
policy: push
script:
- - retry gem install fog-aws mime-types activesupport --no-document
+ - retry gem install fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- FLAKY_RSPEC_GENERATE_REPORT=1 scripts/prune-old-flaky-specs ${FLAKY_RSPEC_SUITE_REPORT_PATH}
@@ -358,6 +361,7 @@ update-tests-metadata:
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $FLAKY_RSPEC_SUITE_REPORT_PATH'
- rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
- rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
+ - scripts/insert-rspec-profiling-data
flaky-examples-check:
<<: *dedicated-runner
@@ -484,6 +488,9 @@ setup-test-env:
build-qa-image:
<<: *review-docker
+ variables:
+ <<: *review-docker-variables
+ GIT_DEPTH: "20"
stage: prepare
script:
- time docker build --cache-from ${LATEST_QA_IMAGE} --tag ${QA_IMAGE} ./qa/
@@ -687,6 +694,7 @@ gitlab:ui:visual:
tags:
- gitlab-org
before_script: []
+ allow_failure: true
dependencies:
- compile-assets
script:
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md
index eef1e877ff2..b4007c1ba7b 100644
--- a/.gitlab/issue_templates/Feature proposal.md
+++ b/.gitlab/issue_templates/Feature proposal.md
@@ -2,32 +2,10 @@
<!-- What problem do we solve? -->
-### Target audience
+### Intended users
-<!--- For whom are we doing this? Include a [persona](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/)
-listed below, if applicable, along with its [label](https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A),
-or define a specific company role, e.g. "Release Manager".
-
-Existing personas are: (copy relevant personas out of this comment, and delete any persona that does not apply)
-
-- Parker, Product Manager, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#parker-product-manager
-/label ~"Persona: Product Manager"
-
-- Delaney, Development Team Lead, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#delaney-development-team-lead
-/label ~"Persona: Development Team Lead"
-
-- Sasha, Software Developer, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#sasha-software-developer
-/label ~"Persona: Software developer"
-
-- Devon, DevOps Engineer, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#devon-devops-engineer
-/label ~"Persona: DevOps Engineer"
-
-- Sidney, Systems Administrator, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#sidney-systems-administrator
-/label ~"Persona: Systems Administrator"
-
-- Sam, Security Analyst, https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas#sam-security-analyst
-/label ~"Persona: Security Analyst"
--->
+<!-- Who will use this feature? If known, include any of the following: types of users (e.g. Developer), personas, or specific company roles (e.g. Release Manager). It's okay to write "Unknown" and fill this field in later.
+Personas can be found at https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/ -->
### Further details
diff --git a/.prettierrc b/.prettierrc
index 3384551aea5..5e2863a11f6 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,13 +1,5 @@
{
"printWidth": 100,
"singleQuote": true,
- "trailingComma": "es5",
- "overrides": [
- {
- "files": ["**/app/**/*", "**/spec/**/*"],
- "options": {
- "trailingComma": "all"
- }
- }
- ]
+ "trailingComma": "all"
}
diff --git a/.rubocop.yml b/.rubocop.yml
index 2985c1446e4..9143966b864 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -8,6 +8,7 @@ require:
- rubocop-rspec
AllCops:
+ TargetRubyVersion: 2.5
TargetRailsVersion: 5.0
Exclude:
- 'vendor/**/*'
@@ -184,3 +185,8 @@ Cop/InjectEnterpriseEditionModule:
Style/ReturnNil:
Enabled: true
+
+# It isn't always safe to replace `=~` with `.match?`, especially when there are
+# nil values on the left hand side
+Performance/RegexpMatch:
+ Enabled: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a4a6c9ff13..bec6a8b3e21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,21 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 11.8.2 (2019-03-13)
+
+### Security (1 change)
+
+- Fixed ability to see private groups by users not belonging to given group.
+
+### Fixed (5 changes)
+
+- Fix import_jid error on project import. !25239
+- Properly handle multiple X-Forwarded-For addresses in runner IP. !25511
+- Fix error when viewing group issue boards when user doesn't have explicit group permissions. !25524
+- Fix method to mark a project repository as writable. !25546
+- Allow project members to see private group if the project is in the group namespace.
+
+
## 11.8.0 (2019-02-22)
### Security (7 changes, 1 of them is from the community)
diff --git a/Dangerfile b/Dangerfile
index 715a2bcbbae..32f4b4d23c3 100644
--- a/Dangerfile
+++ b/Dangerfile
@@ -12,3 +12,4 @@ danger.import_dangerfile(path: 'danger/duplicate_yarn_dependencies')
danger.import_dangerfile(path: 'danger/prettier')
danger.import_dangerfile(path: 'danger/eslint')
danger.import_dangerfile(path: 'danger/roulette')
+danger.import_dangerfile(path: 'danger/single_codebase')
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 5ff8c4f5d2a..5db08bf2dc5 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.26.0
+1.27.0
diff --git a/Gemfile b/Gemfile
index caa40e99307..20c8ffa9cb3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -42,11 +42,11 @@ gem 'omniauth-shibboleth', '~> 1.3.0'
gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.3'
-gem 'rack-oauth2', '~> 1.2.1'
+gem 'rack-oauth2', '~> 1.9.3'
gem 'jwt', '~> 2.1.0'
# Spam and anti-bot protection
-gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
+gem 'recaptcha', '~> 4.11', require: 'recaptcha/rails'
gem 'akismet', '~> 2.0'
# Two-factor authentication
@@ -265,7 +265,6 @@ gem 'addressable', '~> 2.5.2'
gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3'
gem 'gon', '~> 6.2'
-gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'request_store', '~> 1.3'
gem 'virtus', '~> 1.0.1'
gem 'base32', '~> 0.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 634029e03de..ec34d3f9d67 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -65,7 +65,7 @@ GEM
atomic (1.1.99)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
- attr_required (1.0.0)
+ attr_required (1.0.1)
awesome_print (1.8.0)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
@@ -398,7 +398,6 @@ GEM
activesupport
multipart-post
oauth (~> 0.5, >= 0.5.0)
- jquery-atwho-rails (1.3.2)
js_regex (3.1.1)
character_set (~> 1.1)
regexp_parser (~> 1.1)
@@ -622,12 +621,12 @@ GEM
rack-attack (4.4.1)
rack
rack-cors (1.0.2)
- rack-oauth2 (1.2.3)
- activesupport (>= 2.3)
- attr_required (>= 0.0.5)
- httpclient (>= 2.4)
- multi_json (>= 1.3.6)
- rack (>= 1.1)
+ rack-oauth2 (1.9.3)
+ activesupport
+ attr_required
+ httpclient
+ json-jwt (>= 1.9.0)
+ rack
rack-protection (2.0.5)
rack
rack-proxy (0.6.0)
@@ -680,7 +679,7 @@ GEM
optimist (>= 3.0.0)
rdoc (6.0.4)
re2 (1.1.1)
- recaptcha (3.0.0)
+ recaptcha (4.13.1)
json
recursive-open-struct (1.1.0)
redis (3.3.5)
@@ -1046,7 +1045,6 @@ DEPENDENCIES
influxdb (~> 0.2)
jaeger-client (~> 0.10.0)
jira-ruby (~> 1.4)
- jquery-atwho-rails (~> 1.3.2)
js_regex (~> 3.1)
json-schema (~> 2.8.0)
jwt (~> 2.1.0)
@@ -1103,7 +1101,7 @@ DEPENDENCIES
rack (= 2.0.6)
rack-attack (~> 4.4.1)
rack-cors (~> 1.0.0)
- rack-oauth2 (~> 1.2.1)
+ rack-oauth2 (~> 1.9.3)
rack-proxy (~> 0.6.0)
rails (= 5.0.7.1)
rails-controller-testing
@@ -1115,7 +1113,7 @@ DEPENDENCIES
rbtrace (~> 0.4)
rdoc (~> 6.0)
re2 (~> 1.1.1)
- recaptcha (~> 3.0)
+ recaptcha (~> 4.11)
redis (~> 3.2)
redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2)
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 13e8617c515..5b206b82fe0 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,6 +1,7 @@
<script>
import _ from 'underscore';
import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg';
+import { GlLoadingIcon } from '@gitlab/ui';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png';
@@ -23,6 +24,7 @@ export default {
applicationRow,
clipboardButton,
LoadingButton,
+ GlLoadingIcon,
},
props: {
type: {
@@ -296,7 +298,12 @@ export default {
/>
</span>
</div>
- <input v-else type="text" class="form-control js-endpoint" readonly value="?" />
+ <div v-else class="input-group">
+ <input type="text" class="form-control js-endpoint" readonly />
+ <gl-loading-icon
+ class="position-absolute align-self-center ml-2 js-ingress-ip-loading-icon"
+ />
+ </div>
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Point a wildcard DNS to this
@@ -545,13 +552,12 @@ export default {
/>
</span>
</div>
- <input
- v-else
- type="text"
- class="form-control js-knative-endpoint"
- readonly
- value="?"
- />
+ <div v-else class="input-group">
+ <input type="text" class="form-control js-endpoint" readonly />
+ <gl-loading-icon
+ class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
+ />
+ </div>
</div>
<p class="form-text text-muted col-12">
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
index 009153d0703..2f268419bff 100644
--- a/app/assets/javascripts/commons/jquery.js
+++ b/app/assets/javascripts/commons/jquery.js
@@ -3,7 +3,7 @@ import 'jquery';
// common jQuery plugins
import 'jquery-ujs';
import 'vendor/jquery.endless-scroll';
-import 'vendor/jquery.caret';
-import 'vendor/jquery.atwho';
+import 'jquery.caret'; // must be imported before at.js
+import 'at.js';
import 'vendor/jquery.scrollTo';
import 'jquery.waitforimages';
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index bffc025ced3..a0ca44caa07 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -7,6 +7,7 @@ import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
import 'core-js/fn/object/values';
import 'core-js/fn/promise';
+import 'core-js/fn/promise/finally';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
import 'core-js/fn/string/includes';
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 0bf2dde8b96..fe49dfff10b 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -125,9 +125,9 @@ export default {
>
{{ __('Show latest version') }}
</gl-button>
- <a v-show="hasCollapsedFile" class="btn btn-default append-right-8" @click="expandAllFiles">
+ <gl-button v-show="hasCollapsedFile" class="append-right-8" @click="expandAllFiles">
{{ __('Expand all') }}
- </a>
+ </gl-button>
<settings-dropdown />
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 1141a197c6a..0e779e1be9a 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -73,13 +73,23 @@ export default {
if (!newVal && oldVal && !this.hasDiffLines) {
this.handleLoadCollapsedDiff();
}
+
+ this.setFileCollapsed({ filePath: this.file.file_path, collapsed: newVal });
+ },
+ 'file.viewer.collapsed': function setIsCollapsed(newVal) {
+ this.isCollapsed = newVal;
},
},
created() {
eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.handleLoadCollapsedDiff);
},
methods: {
- ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff', 'setRenderIt']),
+ ...mapActions('diffs', [
+ 'loadCollapsedDiff',
+ 'assignDiscussionsToDiff',
+ 'setRenderIt',
+ 'setFileCollapsed',
+ ]),
handleToggle() {
if (!this.hasDiffLines) {
this.handleLoadCollapsedDiff();
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 57ddc923a3e..4a04216d893 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -344,5 +344,8 @@ export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => {
}
};
+export const setFileCollapsed = ({ commit }, { filePath, collapsed }) =>
+ commit(types.SET_FILE_COLLAPSED, { filePath, collapsed });
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index b441b1de451..adf56eba3f8 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -27,3 +27,4 @@ export const TOGGLE_FILE_FINDER_VISIBLE = 'TOGGLE_FILE_FINDER_VISIBLE';
export const REQUEST_FULL_DIFF = 'REQUEST_FULL_DIFF';
export const RECEIVE_FULL_DIFF_SUCCESS = 'RECEIVE_FULL_DIFF_SUCCESS';
export const RECEIVE_FULL_DIFF_ERROR = 'RECEIVE_FULL_DIFF_ERROR';
+export const SET_FILE_COLLAPSED = 'SET_FILE_COLLAPSED';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 45187d93fef..856f4eaf00a 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -104,7 +104,10 @@ export default {
[types.EXPAND_ALL_FILES](state) {
state.diffFiles = state.diffFiles.map(file => ({
...file,
- collapsed: false,
+ viewer: {
+ ...file.viewer,
+ collapsed: false,
+ },
}));
},
@@ -300,4 +303,11 @@ export default {
}),
});
},
+ [types.SET_FILE_COLLAPSED](state, { filePath, collapsed }) {
+ const file = state.diffFiles.find(f => f.file_path === filePath);
+
+ if (file && file.viewer) {
+ file.viewer.collapsed = collapsed;
+ }
+ },
};
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 36542315c4c..b9b3b344524 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -3,6 +3,7 @@ import createFlash from '~/flash';
import { s__ } from '~/locale';
import emojiAliases from 'emojis/aliases.json';
import axios from '../lib/utils/axios_utils';
+import csrf from '../lib/utils/csrf';
import AccessorUtilities from '../lib/utils/accessor';
@@ -24,11 +25,15 @@ export function initEmojiMap() {
resolve(emojiMap);
} else {
// We load the JSON from server
- axios
- .get(
- `${gon.asset_host || ''}${gon.relative_url_root ||
- ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
- )
+ const axiosInstance = axios.create();
+
+ // If the static JSON file is on a CDN we don't want to send the default CSRF token
+ if (gon.asset_host) {
+ delete axiosInstance.defaults.headers.common[csrf.headerKey];
+ }
+
+ axiosInstance
+ .get(`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`)
.then(({ data }) => {
emojiMap = data;
validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 6ece8b92a30..be80661223c 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -1,14 +1,16 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import tablePagination from '../../vue_shared/components/table_pagination.vue';
-import environmentTable from '../components/environments_table.vue';
+import TablePagination from '~/vue_shared/components/table_pagination.vue';
+import containerMixin from 'ee_else_ce/environments/mixins/container_mixin';
+import EnvironmentTable from '../components/environments_table.vue';
export default {
components: {
- environmentTable,
- tablePagination,
+ EnvironmentTable,
+ TablePagination,
GlLoadingIcon,
},
+ mixins: [containerMixin],
props: {
isLoading: {
type: Boolean,
@@ -47,7 +49,15 @@ export default {
<slot name="emptyState"></slot>
<div v-if="!isLoading && environments.length > 0" class="table-holder">
- <environment-table :environments="environments" :can-read-environment="canReadEnvironment" />
+ <environment-table
+ :environments="environments"
+ :can-read-environment="canReadEnvironment"
+ :canary-deployment-feature-id="canaryDeploymentFeatureId"
+ :show-canary-deployment-callout="showCanaryDeploymentCallout"
+ :user-callouts-path="userCalloutsPath"
+ :lock-promotion-svg-path="lockPromotionSvgPath"
+ :help-canary-deployments-path="helpCanaryDeploymentsPath"
+ />
<table-pagination
v-if="pagination && pagination.totalPages > 1"
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 1e89dce69cb..a092bdfbc6c 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -4,6 +4,7 @@ import _ from 'underscore';
import { GlTooltipDirective } from '@gitlab/ui';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import Icon from '~/vue_shared/components/icon.vue';
+import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
@@ -34,10 +35,10 @@ export default {
TerminalButtonComponent,
MonitoringButtonComponent,
},
-
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [environmentItemMixin],
props: {
model: {
@@ -467,9 +468,18 @@ export default {
<div v-if="!model.isFolder" class="table-mobile-header" role="rowheader">
{{ s__('Environments|Environment') }}
</div>
+
+ <span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard">
+ <icon :name="deployIconName" />
+ </span>
+
<span v-if="!model.isFolder" class="environment-name table-mobile-content">
<a class="qa-environment-link" :href="environmentPath"> {{ model.name }} </a>
+ <span v-if="isProtected" class="badge badge-success">
+ {{ s__('Environments|protected') }}
+ </span>
</span>
+
<span v-else class="folder-name" role="button" @click="onClickFolder">
<icon :name="folderIconName" class="folder-icon" />
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 6e55c3f901a..ec78240217b 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,4 +1,5 @@
<script>
+import envrionmentsAppMixin from 'ee_else_ce/environments/mixins/environments_app_mixin';
import Flash from '../../flash';
import { s__ } from '../../locale';
import emptyState from './empty_state.vue';
@@ -15,7 +16,7 @@ export default {
ConfirmRollbackModal,
},
- mixins: [CIPaginationMixin, environmentsMixin],
+ mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
props: {
endpoint: {
@@ -95,9 +96,9 @@ export default {
<tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" />
<div v-if="canCreateEnvironment && !isLoading" class="nav-controls">
- <a :href="newEnvironmentPath" class="btn btn-success">{{
- s__('Environments|New environment')
- }}</a>
+ <a :href="newEnvironmentPath" class="btn btn-success">
+ {{ s__('Environments|New environment') }}
+ </a>
</div>
</div>
@@ -106,6 +107,11 @@ export default {
:environments="state.environments"
:pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
+ :canary-deployment-feature-id="canaryDeploymentFeatureId"
+ :show-canary-deployment-callout="showCanaryDeploymentCallout"
+ :user-callouts-path="userCalloutsPath"
+ :lock-promotion-svg-path="lockPromotionSvgPath"
+ :help-canary-deployments-path="helpCanaryDeploymentsPath"
@onChangePage="onChangePage"
>
<empty-state
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index eef141a07ba..ff4e16178e8 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -4,21 +4,24 @@
*/
import { GlLoadingIcon } from '@gitlab/ui';
import _ from 'underscore';
-import environmentItem from './environment_item.vue';
+import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
+import EnvironmentItem from './environment_item.vue';
export default {
components: {
- environmentItem,
+ EnvironmentItem,
GlLoadingIcon,
+ DeployBoard: () => import('ee_component/environments/components/deploy_board_component.vue'),
+ CanaryDeploymentCallout: () =>
+ import('ee_component/environments/components/canary_deployment_callout.vue'),
},
-
+ mixins: [environmentTableMixin],
props: {
environments: {
type: Array,
required: true,
default: () => [],
},
-
canReadEnvironment: {
type: Boolean,
required: false,
@@ -95,6 +98,21 @@ export default {
:can-read-environment="canReadEnvironment"
/>
+ <div
+ v-if="shouldRenderDeployBoard"
+ :key="`deploy-board-row-${i}`"
+ class="js-deploy-board-row"
+ >
+ <div class="deploy-board-container">
+ <deploy-board
+ :deploy-board-data="model.deployBoardData"
+ :is-loading="model.isLoadingDeployBoard"
+ :is-empty="model.isEmptyDeployBoard"
+ :logs-path="model.logs_path"
+ />
+ </div>
+ </div>
+
<template v-if="shouldRenderFolderContent(model)">
<div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`">
<gl-loading-icon :size="2" class="prepend-top-16" />
@@ -111,13 +129,24 @@ export default {
<div :key="`sub-div-${i}`">
<div class="text-center prepend-top-10">
- <a :href="folderUrl(model)" class="btn btn-default">{{
- s__('Environments|Show all')
- }}</a>
+ <a :href="folderUrl(model)" class="btn btn-default">
+ {{ s__('Environments|Show all') }}
+ </a>
</div>
</div>
</template>
</template>
+
+ <template v-if="shouldShowCanaryCallout(model)">
+ <canary-deployment-callout
+ :key="`canary-promo-${i}`"
+ :canary-deployment-feature-id="canaryDeploymentFeatureId"
+ :user-callouts-path="userCalloutsPath"
+ :lock-promotion-svg-path="lockPromotionSvgPath"
+ :help-canary-deployments-path="helpCanaryDeploymentsPath"
+ :data-js-canary-promo-key="i"
+ />
+ </template>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 56e7f69cad6..c1bfe8d05fe 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import canaryCalloutMixin from 'ee_else_ce/environments/mixins/canary_callout_mixin';
import environmentsFolderApp from './environments_folder_view.vue';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
@@ -11,6 +12,7 @@ export default () =>
components: {
environmentsFolderApp,
},
+ mixins: [canaryCalloutMixin],
data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
@@ -28,6 +30,7 @@ export default () =>
folderName: this.folderName,
cssContainerClass: this.cssContainerClass,
canReadEnvironment: this.canReadEnvironment,
+ ...this.canaryCalloutProps,
},
});
},
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 80f0e00400b..6fd0561f682 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,4 +1,5 @@
<script>
+import folderMixin from 'ee_else_ce/environments/mixins/environments_folder_view_mixin';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
@@ -8,7 +9,7 @@ export default {
StopEnvironmentModal,
},
- mixins: [environmentsMixin, CIPaginationMixin],
+ mixins: [environmentsMixin, CIPaginationMixin, folderMixin],
props: {
endpoint: {
@@ -41,7 +42,8 @@ export default {
<div v-if="!isLoading" class="top-area">
<h4 class="js-folder-name environments-folder-name">
- {{ s__('Environments|Environments') }} / <b>{{ folderName }}</b>
+ {{ s__('Environments|Environments') }} /
+ <b>{{ folderName }}</b>
</h4>
<tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" />
@@ -52,6 +54,11 @@ export default {
:environments="state.environments"
:pagination="state.paginationInformation"
:can-read-environment="canReadEnvironment"
+ :canary-deployment-feature-id="canaryDeploymentFeatureId"
+ :show-canary-deployment-callout="showCanaryDeploymentCallout"
+ :user-callouts-path="userCalloutsPath"
+ :lock-promotion-svg-path="lockPromotionSvgPath"
+ :help-canary-deployments-path="helpCanaryDeploymentsPath"
@onChangePage="onChangePage"
/>
</div>
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index 6af66d0f86e..b53d42f202b 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import canaryCalloutMixin from 'ee_else_ce/environments/mixins/canary_callout_mixin';
import environmentsComponent from './components/environments_app.vue';
import { parseBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
@@ -11,6 +12,7 @@ export default () =>
components: {
environmentsComponent,
},
+ mixins: [canaryCalloutMixin],
data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
@@ -32,6 +34,7 @@ export default () =>
cssContainerClass: this.cssContainerClass,
canCreateEnvironment: this.canCreateEnvironment,
canReadEnvironment: this.canReadEnvironment,
+ ...this.canaryCalloutProps,
},
});
},
diff --git a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
new file mode 100644
index 00000000000..f6d3d67b777
--- /dev/null
+++ b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
@@ -0,0 +1,5 @@
+export default {
+ computed: {
+ canaryCalloutProps() {},
+ },
+};
diff --git a/app/assets/javascripts/environments/mixins/container_mixin.js b/app/assets/javascripts/environments/mixins/container_mixin.js
new file mode 100644
index 00000000000..f2907c120f8
--- /dev/null
+++ b/app/assets/javascripts/environments/mixins/container_mixin.js
@@ -0,0 +1,29 @@
+export default {
+ props: {
+ canaryDeploymentFeatureId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ showCanaryDeploymentCallout: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ userCalloutsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ lockPromotionSvgPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ helpCanaryDeploymentsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
diff --git a/app/assets/javascripts/environments/mixins/environment_item_mixin.js b/app/assets/javascripts/environments/mixins/environment_item_mixin.js
new file mode 100644
index 00000000000..2dfed36ec99
--- /dev/null
+++ b/app/assets/javascripts/environments/mixins/environment_item_mixin.js
@@ -0,0 +1,13 @@
+export default {
+ computed: {
+ deployIconName() {
+ return '';
+ },
+ shouldRenderDeployBoard() {
+ return false;
+ },
+ },
+ methods: {
+ toggleDeployBoard() {},
+ },
+};
diff --git a/app/assets/javascripts/environments/mixins/environments_app_mixin.js b/app/assets/javascripts/environments/mixins/environments_app_mixin.js
new file mode 100644
index 00000000000..fc805b9235a
--- /dev/null
+++ b/app/assets/javascripts/environments/mixins/environments_app_mixin.js
@@ -0,0 +1,32 @@
+export default {
+ props: {
+ canaryDeploymentFeatureId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCanaryDeploymentCallout: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ userCalloutsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lockPromotionSvgPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ helpCanaryDeploymentsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ metods: {
+ toggleDeployBoard() {},
+ },
+};
diff --git a/app/assets/javascripts/environments/mixins/environments_folder_view_mixin.js b/app/assets/javascripts/environments/mixins/environments_folder_view_mixin.js
new file mode 100644
index 00000000000..e793a7cadf2
--- /dev/null
+++ b/app/assets/javascripts/environments/mixins/environments_folder_view_mixin.js
@@ -0,0 +1,29 @@
+export default {
+ props: {
+ canaryDeploymentFeatureId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCanaryDeploymentCallout: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ userCalloutsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lockPromotionSvgPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ helpCanaryDeploymentsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 71b6b578196..a5812b173dc 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -3,13 +3,13 @@
*/
import _ from 'underscore';
import Visibility from 'visibilityjs';
+import EnvironmentsStore from 'ee_else_ce/environments/stores/environments_store';
import Poll from '../../lib/utils/poll';
import { getParameterByName } from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
import Flash from '../../flash';
import eventHub from '../event_hub';
-import EnvironmentsStore from '../stores/environments_store';
import EnvironmentsService from '../services/environments_service';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
diff --git a/app/assets/javascripts/environments/mixins/environments_table_mixin.js b/app/assets/javascripts/environments/mixins/environments_table_mixin.js
new file mode 100644
index 00000000000..208f1a7373d
--- /dev/null
+++ b/app/assets/javascripts/environments/mixins/environments_table_mixin.js
@@ -0,0 +1,10 @@
+export default {
+ methods: {
+ shouldShowCanaryCallout() {
+ return false;
+ },
+ shouldRenderDeployBoard() {
+ return false;
+ },
+ },
+};
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index ac9a31c202c..5fb420e9da5 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,4 +1,6 @@
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { setDeployBoard } from 'ee_else_ce/environments/stores/helpers';
+
/**
* Environments Store.
*
@@ -31,6 +33,14 @@ export default class EnvironmentsStore {
* If the `size` is bigger than 1, it means it should be rendered as a folder.
* In those cases we add `isFolder` key in order to render it properly.
*
+ * Top level environments - when the size is 1 - with `rollout_status`
+ * can render a deploy board. We add `isDeployBoardVisible` and `deployBoardData`
+ * keys to those environments.
+ * The first key will let's us know if we should or not render the deploy board.
+ * It will be toggled when the user clicks to seee the deploy board.
+ *
+ * The second key will allow us to update the environment with the received deploy board data.
+ *
* @param {Array} environments
* @returns {Array}
*/
@@ -63,6 +73,7 @@ export default class EnvironmentsStore {
filtered = Object.assign(filtered, env);
}
+ filtered = setDeployBoard(oldEnvironmentState, filtered);
return filtered;
});
@@ -71,6 +82,20 @@ export default class EnvironmentsStore {
return filteredEnvironments;
}
+ /**
+ * Stores the pagination information needed to render the pagination for the
+ * table.
+ *
+ * Normalizes the headers to uppercase since they can be provided either
+ * in uppercase or lowercase.
+ *
+ * Parses to an integer the normalized ones needed for the pagination component.
+ *
+ * Stores the normalized and parsed information.
+ *
+ * @param {Object} pagination = {}
+ * @return {Object}
+ */
setPagination(pagination = {}) {
const normalizedHeaders = normalizeHeaders(pagination);
const paginationInformation = parseIntPagination(normalizedHeaders);
diff --git a/app/assets/javascripts/environments/stores/helpers.js b/app/assets/javascripts/environments/stores/helpers.js
new file mode 100644
index 00000000000..8eba6c00601
--- /dev/null
+++ b/app/assets/javascripts/environments/stores/helpers.js
@@ -0,0 +1,8 @@
+/**
+ * Deploy boards are EE only.
+ *
+ * @param {Object} environment
+ * @returns {Object}
+ */
+// eslint-disable-next-line import/prefer-default-export
+export const setDeployBoard = (oldEnvironmentState, environment) => environment;
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 54ea936252e..0b24d9fc920 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -13,4 +13,16 @@ export default IssuableTokenKeys => {
IssuableTokenKeys.tokenKeys.push(wipToken);
IssuableTokenKeys.tokenKeysWithAlternative.push(wipToken);
+
+ const targetBranchToken = {
+ key: 'target-branch',
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'arrow-right',
+ tag: 'branch',
+ };
+
+ IssuableTokenKeys.tokenKeys.push(targetBranchToken);
+ IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken);
};
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index e2f9c03ee65..be867a3838d 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -5,6 +5,7 @@ import DropdownEmoji from './dropdown_emoji';
import NullDropdown from './null_dropdown';
import DropdownAjaxFilter from './dropdown_ajax_filter';
import DropdownUtils from './dropdown_utils';
+import { mergeUrlParams } from '../lib/utils/url_utility';
export default class AvailableDropdownMappings {
constructor(container, baseEndpoint, groupsOnly, includeAncestorGroups, includeDescendantGroups) {
@@ -13,6 +14,7 @@ export default class AvailableDropdownMappings {
this.groupsOnly = groupsOnly;
this.includeAncestorGroups = includeAncestorGroups;
this.includeDescendantGroups = includeDescendantGroups;
+ this.filteredSearchInput = this.container.querySelector('.filtered-search');
}
getAllowedMappings(supportedTokens) {
@@ -102,6 +104,15 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-runner-tag'),
},
+ 'target-branch': {
+ reference: null,
+ gl: DropdownNonUser,
+ extraArguments: {
+ endpoint: this.getMergeRequestTargetBranchesEndpoint(),
+ symbol: '',
+ },
+ element: this.container.querySelector('#js-dropdown-target-branch'),
+ },
};
}
@@ -130,4 +141,24 @@ export default class AvailableDropdownMappings {
getRunnerTagsEndpoint() {
return `${this.baseEndpoint}/admin/runners/tag_list.json`;
}
+
+ getMergeRequestTargetBranchesEndpoint() {
+ const endpoint = `${gon.relative_url_root ||
+ ''}/autocomplete/merge_request_target_branches.json`;
+
+ const params = {
+ group_id: this.getGroupId(),
+ project_id: this.getProjectId(),
+ };
+
+ return mergeUrlParams(params, endpoint);
+ }
+
+ getGroupId() {
+ return this.filteredSearchInput.getAttribute('data-group-id') || '';
+ }
+
+ getProjectId() {
+ return this.filteredSearchInput.getAttribute('data-project-id') || '';
+ }
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 33c82778c79..0c2e87521d9 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -504,14 +504,7 @@ export default class FilteredSearchManager {
const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
if (match) {
- // Use lastIndexOf because the token key is allowed to contain underscore
- // e.g. 'my_reaction' is the token key of 'my_reaction_emoji'
- const lastIndexOf = keyParam.lastIndexOf('_');
- let sanitizedKey = lastIndexOf !== -1 ? keyParam.slice(0, lastIndexOf) : keyParam;
- // Replace underscore with hyphen in the sanitizedkey.
- // e.g. 'my_reaction' => 'my-reaction'
- sanitizedKey = sanitizedKey.replace('_', '-');
- const { symbol } = match;
+ const { key, symbol } = match;
let quotationsToUse = '';
if (sanitizedValue.indexOf(' ') !== -1) {
@@ -520,10 +513,10 @@ export default class FilteredSearchManager {
}
hasFilteredSearch = true;
- const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
+ const canEdit = this.canEdit && this.canEdit(key, sanitizedValue);
const { uppercaseTokenName, capitalizeTokenValue } = match;
FilteredSearchVisualTokens.addFilterVisualToken(
- sanitizedKey,
+ key,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
{
canEdit,
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index 7746908714e..315cd6f64da 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -69,11 +69,21 @@ export default class FilteredSearchVisualTokens {
}
static addVisualTokenElement(name, value, options = {}) {
- const { isSearchTerm = false, canEdit, uppercaseTokenName, capitalizeTokenValue } = options;
+ const {
+ isSearchTerm = false,
+ canEdit,
+ uppercaseTokenName,
+ capitalizeTokenValue,
+ tokenClass = `search-token-${name.toLowerCase()}`,
+ } = options;
const li = document.createElement('li');
li.classList.add('js-visual-token');
li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
+ if (!isSearchTerm) {
+ li.classList.add(tokenClass);
+ }
+
if (value) {
li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({
canEdit,
diff --git a/app/assets/javascripts/lib/utils/simple_poll.js b/app/assets/javascripts/lib/utils/simple_poll.js
index 473f179ad86..576a9ec880c 100644
--- a/app/assets/javascripts/lib/utils/simple_poll.js
+++ b/app/assets/javascripts/lib/utils/simple_poll.js
@@ -1,10 +1,10 @@
-export default (fn, interval = 2000, timeout = 60000) => {
+export default (fn, { interval = 2000, timeout = 60000 } = {}) => {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => {
- if (Date.now() - startTime < timeout) {
+ if (timeout === 0 || Date.now() - startTime < timeout) {
setTimeout(fn.bind(null, next, stop), interval);
} else {
reject(new Error('SIMPLE_POLL_TIMEOUT'));
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index 308ad9784e4..a4dad6f1615 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -6,5 +6,5 @@ export function resetServiceWorkersPublicPath() {
// see: https://webpack.js.org/guides/public-path/
const relativeRootPath = (gon && gon.relative_url_root) || '';
const webpackAssetPath = `${relativeRootPath}/assets/webpack/`;
- __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
+ window.__webpack_public_path__ = webpackAssetPath; // eslint-disable-line
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index c9c01354333..94d2e2b53e9 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -11,8 +11,8 @@ import $ from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
import Autosize from 'autosize';
-import 'vendor/jquery.caret'; // required by jquery.atwho
-import 'vendor/jquery.atwho';
+import 'jquery.caret'; // required by at.js
+import 'at.js';
import AjaxCache from '~/lib/utils/ajax_cache';
import Vue from 'vue';
import syntaxHighlight from '~/syntax_highlight';
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
index 46661e06f6d..889731df180 100644
--- a/app/assets/javascripts/notes/components/discussion_filter_note.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -34,7 +34,7 @@ export default {
<template>
<li class="timeline-entry note note-wrapper discussion-filter-note js-discussion-filter-note">
- <div class="timeline-icon">
+ <div class="timeline-icon d-none d-lg-flex">
<icon name="comment" />
</div>
<div class="timeline-content">
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 68b753a4abf..5c59c0c32dd 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -87,27 +87,25 @@ export default {
<span class="note-headline-light">@{{ author.username }}</span>
</a>
<span v-else>{{ __('A deleted user') }}</span>
- <span class="note-headline-light">
- <span class="note-headline-meta">
- <span class="system-note-message"> <slot></slot> </span>
- <template v-if="createdAt">
- <span class="system-note-separator">
- <template v-if="actionText">{{ actionText }}</template>
- </span>
- <a
- :href="noteTimestampLink"
- class="note-timestamp system-note-separator"
- @click="updateTargetNoteHash"
- >
- <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" />
- </a>
- </template>
- <i
- class="fa fa-spinner fa-spin editing-spinner"
- aria-label="Comment is being updated"
- aria-hidden="true"
- ></i>
- </span>
+ <span class="note-headline-light note-headline-meta">
+ <span class="system-note-message"> <slot></slot> </span>
+ <template v-if="createdAt">
+ <span class="system-note-separator">
+ <template v-if="actionText">{{ actionText }}</template>
+ </span>
+ <a
+ :href="noteTimestampLink"
+ class="note-timestamp system-note-separator"
+ @click="updateTargetNoteHash"
+ >
+ <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" />
+ </a>
+ </template>
+ <i
+ class="fa fa-spinner fa-spin editing-spinner"
+ aria-label="Comment is being updated"
+ aria-hidden="true"
+ ></i>
</span>
</div>
</template>
diff --git a/app/assets/javascripts/pages/groups/details/index.js b/app/assets/javascripts/pages/groups/details/index.js
new file mode 100644
index 00000000000..3bcaa0f0232
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/details/index.js
@@ -0,0 +1,5 @@
+import initGroupDetails from '../shared/group_details';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initGroupDetails('details');
+});
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
new file mode 100644
index 00000000000..01ef3f1db2b
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -0,0 +1,31 @@
+/* eslint-disable no-new */
+
+import { getPagePath } from '~/lib/utils/common_utils';
+import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
+import NewGroupChild from '~/groups/new_group_child';
+import notificationsDropdown from '~/notifications_dropdown';
+import NotificationsForm from '~/notifications_form';
+import ProjectsList from '~/projects_list';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
+import GroupTabs from './group_tabs';
+
+export default function initGroupDetails(actionName = 'show') {
+ const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
+ const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED];
+ const paths = window.location.pathname.split('/');
+ const subpath = paths[paths.length - 1];
+ let action = loadableActions.includes(subpath) ? subpath : getPagePath(1);
+ if (actionName && action === actionName) {
+ action = 'show'; // 'show' resets GroupTabs to default action through base class
+ }
+
+ new GroupTabs({ parentEl: '.groups-listing', action });
+ new ShortcutsNavigation();
+ new NotificationsForm();
+ notificationsDropdown();
+ new ProjectsList();
+
+ if (newGroupChildWrapper) {
+ new NewGroupChild(newGroupChildWrapper);
+ }
+}
diff --git a/app/assets/javascripts/pages/groups/show/group_tabs.js b/app/assets/javascripts/pages/groups/shared/group_tabs.js
index c6fe61d2bd9..c6fe61d2bd9 100644
--- a/app/assets/javascripts/pages/groups/show/group_tabs.js
+++ b/app/assets/javascripts/pages/groups/shared/group_tabs.js
diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js
index 3a45fd70d02..af924e74f1f 100644
--- a/app/assets/javascripts/pages/groups/show/index.js
+++ b/app/assets/javascripts/pages/groups/show/index.js
@@ -1,28 +1,5 @@
-/* eslint-disable no-new */
-
-import { getPagePath } from '~/lib/utils/common_utils';
-import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
-import NewGroupChild from '~/groups/new_group_child';
-import notificationsDropdown from '~/notifications_dropdown';
-import NotificationsForm from '~/notifications_form';
-import ProjectsList from '~/projects_list';
-import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import GroupTabs from './group_tabs';
+import initGroupDetails from '../shared/group_details';
document.addEventListener('DOMContentLoaded', () => {
- const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
- const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED];
- const paths = window.location.pathname.split('/');
- const subpath = paths[paths.length - 1];
- const action = loadableActions.includes(subpath) ? subpath : getPagePath(1);
-
- new GroupTabs({ parentEl: '.groups-listing', action });
- new ShortcutsNavigation();
- new NotificationsForm();
- notificationsDropdown();
- new ProjectsList();
-
- if (newGroupChildWrapper) {
- new NewGroupChild(newGroupChildWrapper);
- }
+ initGroupDetails();
});
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 636308c5401..7f800d20835 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -91,6 +91,7 @@ export default class UserTabs {
this.actions = Object.keys(this.loaded);
this.bindEvents();
+ // TODO: refactor to make this configurable via constructor params with a default value of 'show'
if (this.action === 'show') {
this.action = this.defaultAction;
}
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 09a50d25020..348c407f1b5 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -1,5 +1,6 @@
<script>
import _ from 'underscore';
+import stageColumnMixin from 'ee_else_ce/pipelines/mixins/stage_column_mixin';
import JobItem from './job_item.vue';
import JobGroupDropdown from './job_group_dropdown.vue';
@@ -8,6 +9,7 @@ export default {
JobItem,
JobGroupDropdown,
},
+ mixins: [stageColumnMixin],
props: {
title: {
type: String,
@@ -32,9 +34,6 @@ export default {
groupId(group) {
return `ci-badge-${_.escape(group.name)}`;
},
- buildConnnectorClass(index) {
- return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
- },
pipelineActionRequestComplete() {
this.$emit('refreshPipelineGraph');
},
diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
new file mode 100644
index 00000000000..9177943f88a
--- /dev/null
+++ b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
@@ -0,0 +1,6 @@
+export default {
+ methods: {
+ clickTriggeredByPipeline() {},
+ clickTriggeredPipeline() {},
+ },
+};
diff --git a/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js b/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js
new file mode 100644
index 00000000000..64283ed0e58
--- /dev/null
+++ b/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js
@@ -0,0 +1,7 @@
+export default {
+ methods: {
+ buildConnnectorClass(index) {
+ return index === 0 && !this.isFirstColumn ? 'left-connector' : '';
+ },
+ },
+};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index dc9befe6349..8adbd39edd4 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -2,8 +2,9 @@ import Vue from 'vue';
import Flash from '~/flash';
import Translate from '~/vue_shared/translate';
import { __ } from '~/locale';
+import pipelineGraph from 'ee_else_ce/pipelines/components/graph/graph_component.vue';
+import GraphEEMixin from 'ee_else_ce/pipelines/mixins/graph_pipeline_bundle_mixin';
import PipelinesMediator from './pipeline_details_mediator';
-import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
@@ -22,6 +23,7 @@ export default () => {
components: {
pipelineGraph,
},
+ mixins: [GraphEEMixin],
data() {
return {
mediator,
@@ -44,6 +46,10 @@ export default () => {
},
on: {
refreshPipelineGraph: this.requestRefreshPipelineGraph,
+ onClickTriggeredBy: (parentPipeline, pipeline) =>
+ this.clickTriggeredByPipeline(parentPipeline, pipeline),
+ onClickTriggered: (parentPipeline, pipeline) =>
+ this.clickTriggeredPipeline(parentPipeline, pipeline),
},
});
},
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index bd1e1895660..d67d88c4dba 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -19,6 +19,7 @@ export default class pipelinesMediator {
this.poll = new Poll({
resource: this.service,
method: 'getPipeline',
+ data: this.store.state.expandedPipelines ? this.getExpandedParameters() : undefined,
successCallback: this.successCallback.bind(this),
errorCallback: this.errorCallback.bind(this),
});
@@ -56,6 +57,19 @@ export default class pipelinesMediator {
.getPipeline()
.then(response => this.successCallback(response))
.catch(() => this.errorCallback())
- .finally(() => this.poll.restart());
+ .finally(() =>
+ this.poll.restart(
+ this.store.state.expandedPipelines ? this.getExpandedParameters() : undefined,
+ ),
+ );
+ }
+
+ /**
+ * Backend expects paramets in the following format: `expanded[]=id&expanded[]=id`
+ */
+ getExpandedParameters() {
+ return {
+ expanded: this.store.state.expandedPipelines,
+ };
}
}
diff --git a/app/assets/javascripts/pipelines/services/pipeline_service.js b/app/assets/javascripts/pipelines/services/pipeline_service.js
index a53a9cc8365..e44eb9cdfd1 100644
--- a/app/assets/javascripts/pipelines/services/pipeline_service.js
+++ b/app/assets/javascripts/pipelines/services/pipeline_service.js
@@ -5,8 +5,8 @@ export default class PipelineService {
this.pipeline = endpoint;
}
- getPipeline() {
- return axios.get(this.pipeline);
+ getPipeline(params) {
+ return axios.get(this.pipeline, { params });
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
index 33963d5e1e6..0312b147b62 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -14,6 +14,10 @@ export default {
type: Boolean,
required: true,
},
+ isFastForwardEnabled: {
+ type: Boolean,
+ required: true,
+ },
commitsCount: {
type: Number,
required: false,
@@ -37,16 +41,22 @@ export default {
return n__(__('%d commit'), __('%d commits'), this.isSquashEnabled ? 1 : this.commitsCount);
},
modifyLinkMessage() {
- return this.isSquashEnabled ? __('Modify commit messages') : __('Modify merge commit');
+ if (this.isFastForwardEnabled) return __('Modify commit message');
+ else if (this.isSquashEnabled) return __('Modify commit messages');
+ return __('Modify merge commit');
},
ariaLabel() {
return this.expanded ? __('Collapse') : __('Expand');
},
message() {
+ const message = this.isFastForwardEnabled
+ ? s__('mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}.')
+ : s__(
+ 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.',
+ );
+
return sprintf(
- s__(
- 'mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}.',
- ),
+ message,
{
commitCount: `<strong class="commits-count-message">${this.commitsCountMessage}</strong>`,
mergeCommitCount: `<strong>${s__('mrWidgetCommitsAdded|1 merge commit')}</strong>`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 9b4e80ee3a3..bb76eb1030d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -113,6 +113,12 @@ export default {
shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText;
},
+ shouldShowSquashEdit() {
+ return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge;
+ },
+ shouldShowMergeEdit() {
+ return !this.mr.ffOnlyEnabled;
+ },
},
methods: {
updateMergeCommitMessage(includeDescription) {
@@ -159,9 +165,12 @@ export default {
});
},
initiateMergePolling() {
- simplePoll((continuePolling, stopPolling) => {
- this.handleMergePolling(continuePolling, stopPolling);
- });
+ simplePoll(
+ (continuePolling, stopPolling) => {
+ this.handleMergePolling(continuePolling, stopPolling);
+ },
+ { timeout: 0 },
+ );
},
handleMergePolling(continuePolling, stopPolling) {
this.service
@@ -192,6 +201,7 @@ export default {
})
.catch(() => {
new Flash(__('Something went wrong while merging this merge request. Please try again.')); // eslint-disable-line
+ stopPolling();
});
},
initiateRemoveSourceBranchPolling() {
@@ -321,43 +331,44 @@ export default {
<div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message">
{{ __('Fast-forward merge without a merge commit') }}
</div>
- <template v-else>
- <commits-header
- :is-squash-enabled="squashBeforeMerge"
- :commits-count="mr.commitsCount"
- :target-branch="mr.targetBranch"
- >
- <ul class="border-top content-list commits-list flex-list">
- <commit-edit
- v-if="squashBeforeMerge && shouldShowSquashBeforeMerge"
+ <commits-header
+ v-if="shouldShowSquashEdit || shouldShowMergeEdit"
+ :is-squash-enabled="squashBeforeMerge"
+ :commits-count="mr.commitsCount"
+ :target-branch="mr.targetBranch"
+ :is-fast-forward-enabled="mr.ffOnlyEnabled"
+ >
+ <ul class="border-top content-list commits-list flex-list">
+ <commit-edit
+ v-if="shouldShowSquashEdit"
+ v-model="squashCommitMessage"
+ :label="__('Squash commit message')"
+ input-id="squash-message-edit"
+ squash
+ >
+ <commit-message-dropdown
+ slot="header"
v-model="squashCommitMessage"
- :label="__('Squash commit message')"
- input-id="squash-message-edit"
- squash
- >
- <commit-message-dropdown
- slot="header"
- v-model="squashCommitMessage"
- :commits="mr.commits"
+ :commits="mr.commits"
+ />
+ </commit-edit>
+ <commit-edit
+ v-if="shouldShowMergeEdit"
+ v-model="commitMessage"
+ :label="__('Merge commit message')"
+ input-id="merge-message-edit"
+ >
+ <label slot="checkbox">
+ <input
+ id="include-description"
+ type="checkbox"
+ @change="updateMergeCommitMessage($event.target.checked)"
/>
- </commit-edit>
- <commit-edit
- v-model="commitMessage"
- :label="__('Merge commit message')"
- input-id="merge-message-edit"
- >
- <label slot="checkbox">
- <input
- id="include-description"
- type="checkbox"
- @change="updateMergeCommitMessage($event.target.checked)"
- />
- {{ __('Include merge request description') }}
- </label>
- </commit-edit>
- </ul>
- </commits-header>
- </template>
+ {{ __('Include merge request description') }}
+ </label>
+ </commit-edit>
+ </ul>
+ </commits-header>
</template>
</div>
</template>
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index d1cf2b8f9a0..86189143525 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -2,7 +2,6 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope.
- *= require jquery.atwho
*= require_self
*= require cropper.css
*/
@@ -15,6 +14,7 @@
* directory.
*/
+@import "../../../node_modules/at.js/dist/css/jquery.atwho";
@import "../../../node_modules/pikaday/scss/pikaday";
@import "../../../node_modules/dropzone/dist/basic";
@import "../../../node_modules/select2/select2";
diff --git a/app/assets/stylesheets/components/dashboard_skeleton.scss b/app/assets/stylesheets/components/dashboard_skeleton.scss
new file mode 100644
index 00000000000..42ede599bc6
--- /dev/null
+++ b/app/assets/stylesheets/components/dashboard_skeleton.scss
@@ -0,0 +1,80 @@
+.dashboard-cards {
+ margin-right: -$gl-padding-8;
+ margin-left: -$gl-padding-8;
+}
+
+.dashboard-card {
+ &-header {
+ &-warning {
+ background-color: $orange-100;
+ }
+
+ &-failed {
+ background-color: $red-100;
+ }
+ }
+
+ &-body {
+ height: 120px;
+
+ &-warning {
+ background-color: $orange-50;
+ }
+
+ &-failed {
+ background-color: $red-50;
+ }
+ }
+
+ &-time-ago {
+ &-icon {
+ color: $gray-500;
+ }
+ }
+
+ &-footer {
+ border-radius: $gl-padding;
+ height: $gl-padding-32;
+
+ &-failed {
+ background-color: $red-100;
+ }
+
+ &-arrow {
+ color: $gray-300;
+ }
+
+ &-downstream {
+ margin-right: -$gl-padding-8;
+ }
+
+ &-extra {
+ background-color: $gray-400;
+ font-size: 10px;
+ line-height: $gl-line-height;
+ width: $gl-padding;
+ }
+ }
+
+ &-skeleton-info {
+ border-radius: $gl-padding;
+ height: $gl-padding;
+ overflow: hidden;
+
+ &::after {
+ content: ' ';
+ display: block;
+ animation: blockTextShine 1s linear infinite forwards;
+ background-repeat: no-repeat;
+ background-size: cover;
+ background-image: linear-gradient(to right,
+ $gray-100 0%,
+ $gray-50 20%,
+ $gray-100 40%,
+ $gray-100 100%);
+ border-radius: $gl-padding;
+ height: $gl-padding;
+ margin-top: -$gl-padding-8;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index aad5150c0b5..d47931a49e4 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -376,18 +376,21 @@ img.emoji {
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-16 { margin-top: 16px; }
.prepend-top-20 { margin-top: 20px; }
+.prepend-top-32 { margin-top: 32px; }
.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; }
.prepend-left-8 { margin-left: 8px; }
.prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left: 20px; }
+.prepend-left-32 { margin-left: 32px; }
.append-right-4 { margin-right: 4px; }
.append-right-5 { margin-right: 5px; }
.append-right-8 { margin-right: 8px; }
.append-right-10 { margin-right: 10px; }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
+.prepend-right-32 { margin-right: 32px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-4 { margin-bottom: $gl-padding-4; }
.append-bottom-5 { margin-bottom: 5px; }
@@ -396,6 +399,7 @@ img.emoji {
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
.append-bottom-default { margin-bottom: $gl-padding; }
+.prepend-bottom-32 { margin-bottom: 32px; }
.inline { display: inline-block; }
.center { text-align: center; }
.vertical-align-middle { vertical-align: middle; }
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index e5b529ae11d..5bcfd5d1322 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -108,6 +108,8 @@
}
.value-container {
+ display: flex;
+ align-items: center;
background-color: $white-normal;
color: $filter-value-text-color;
border-radius: 0 2px 2px 0;
@@ -121,7 +123,7 @@
.remove-token {
display: inline-block;
- padding-left: 4px;
+ padding-left: 8px;
padding-right: 0;
.fa-close {
@@ -412,3 +414,10 @@
padding: 8px 16px;
text-align: center;
}
+
+.search-token-target-branch {
+ .value {
+ font-family: $monospace-font;
+ font-size: 13px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss
index 79d52932719..efcc437bd7f 100644
--- a/app/assets/stylesheets/framework/variables_overrides.scss
+++ b/app/assets/stylesheets/framework/variables_overrides.scss
@@ -42,6 +42,9 @@ $spacers: (
3: ($spacer * 2),
4: ($spacer * 3),
5: ($spacer * 4),
- 6: ($spacer * 8)
+ 6: ($spacer * 5),
+ 7: ($spacer * 6),
+ 8: ($spacer * 7),
+ 9: ($spacer * 8)
);
$pagination-color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 126b00af552..44556060c65 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -1019,3 +1019,13 @@
padding-left: 50px;
padding-bottom: $gl-padding;
}
+
+.mr-compare {
+ .diff-file .file-title-flex-parent {
+ top: $header-height + 51px;
+
+ .with-performance-bar & {
+ top: $performance-bar-height + $header-height + 51px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 72f48e98c24..faf85e151e3 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -283,8 +283,6 @@ $note-form-margin-left: 72px;
}
.system-note-message {
- display: inline;
-
&::first-letter {
text-transform: lowercase;
}
@@ -607,12 +605,6 @@ $note-form-margin-left: 72px;
}
.note-headline-meta {
- display: inline-block;
-
- .system-note-message {
- white-space: normal;
- }
-
.system-note-separator {
color: $gl-text-color-disabled;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 1349845f300..8e53876eb4f 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -693,10 +693,6 @@
}
}
-.project-empty-note-panel {
- border-bottom: 1px solid $border-color;
-}
-
.project-stats,
.project-buttons {
.scrolling-tabs-container {
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 4f9d96da4bd..54126577f93 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -23,7 +23,10 @@
}
.settings {
- border-bottom: 1px solid $gray-darker;
+ // border-top for each item except the top one
+ + .settings {
+ border-top: 1px solid $border-color;
+ }
&:first-of-type {
margin-top: 10px;
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index d331edaa302..79186480605 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -25,6 +25,8 @@
}
#contributors {
+ flex: 1;
+
.contributors-list {
margin: 0 0 10px;
list-style: none;
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 0d5c8657c9e..091327931c2 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class AutocompleteController < ApplicationController
- skip_before_action :authenticate_user!, only: [:users, :award_emojis]
+ skip_before_action :authenticate_user!, only: [:users, :award_emojis, :merge_request_target_branches]
def users
project = Autocomplete::ProjectFinder
@@ -38,4 +38,11 @@ class AutocompleteController < ApplicationController
def award_emojis
render json: AwardedEmojiFinder.new(current_user).execute
end
+
+ def merge_request_target_branches
+ merge_requests = MergeRequestsFinder.new(current_user, params).execute
+ target_branches = merge_requests.recent_target_branches
+
+ render json: target_branches.map { |target_branch| { title: target_branch } }
+ end
end
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index f476f428fdb..f378f7ac79a 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -17,6 +17,16 @@ module Groups
redirect_to group_settings_ci_cd_path
end
+ def update_auto_devops
+ if auto_devops_service.execute
+ flash[:notice] = s_('GroupSettings|Auto DevOps pipeline was updated for the group')
+ else
+ flash[:alert] = s_("GroupSettings|There was a problem updating Auto DevOps pipeline: %{error_messages}." % { error_messages: group.errors.full_messages })
+ end
+
+ redirect_to group_settings_ci_cd_path
+ end
+
private
def define_ci_variables
@@ -29,6 +39,14 @@ module Groups
def authorize_admin_group!
return render_404 unless can?(current_user, :admin_group, group)
end
+
+ def auto_devops_params
+ params.require(:group).permit(:auto_devops_enabled)
+ end
+
+ def auto_devops_service
+ Groups::AutoDevopsService.new(group, current_user, auto_devops_params)
+ end
end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 4e50106398a..0192b1c253e 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -58,11 +58,24 @@ class GroupsController < Groups::ApplicationController
def show
respond_to do |format|
- format.html
+ format.html do
+ render_show_html
+ end
format.atom do
- load_events
- render layout: 'xml.atom'
+ render_details_view_atom
+ end
+ end
+ end
+
+ def details
+ respond_to do |format|
+ format.html do
+ render_details_html
+ end
+
+ format.atom do
+ render_details_view_atom
end
end
end
@@ -119,6 +132,19 @@ class GroupsController < Groups::ApplicationController
protected
+ def render_show_html
+ render 'groups/show'
+ end
+
+ def render_details_html
+ render 'groups/show'
+ end
+
+ def render_details_view_atom
+ load_events
+ render layout: 'xml.atom', template: 'groups/show'
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def authorize_create_group!
allowed = if params[:parent_id].present?
@@ -178,8 +204,8 @@ class GroupsController < Groups::ApplicationController
.includes(:namespace)
@events = EventCollection
- .new(@projects, offset: params[:offset].to_i, filter: event_filter)
- .to_a
+ .new(@projects, offset: params[:offset].to_i, filter: event_filter)
+ .to_a
Events::RenderService
.new(current_user)
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 93bee3f1488..84689ff5dc7 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -29,7 +29,7 @@
#
class MergeRequestsFinder < IssuableFinder
def self.scalar_params
- @scalar_params ||= super + [:wip]
+ @scalar_params ||= super + [:wip, :target_branch]
end
def klass
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index 67e7e475920..0f0d5350df6 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -9,4 +9,17 @@ module AutoDevopsHelper
!project.repository.gitlab_ci_yml &&
!project.ci_service
end
+
+ def badge_for_auto_devops_scope(auto_devops_receiver)
+ return unless auto_devops_receiver.auto_devops_enabled?
+
+ case auto_devops_receiver.first_auto_devops_config[:scope]
+ when :project
+ nil
+ when :group
+ s_('CICD|group enabled')
+ when :instance
+ s_('CICD|instance enabled')
+ end
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 4a9ed123161..9d028dccad7 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -4,6 +4,7 @@ module GroupsHelper
def group_overview_nav_link_paths
%w[
groups#show
+ groups#details
groups#activity
groups#subgroups
analytics#show
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index ca9725f7a04..adffdc0355e 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -13,6 +13,7 @@ module Ci
include EnumWithNil
include HasRef
include ShaAttribute
+ include FromUnion
sha_attribute :source_sha
sha_attribute :target_sha
@@ -185,32 +186,30 @@ module Ci
end
scope :for_user, -> (user) { where(user: user) }
-
- scope :for_merge_request, -> (merge_request, ref, sha) do
- ##
- # We have to filter out unrelated MR pipelines.
- # When merge request is empty, it selects general pipelines, such as push sourced pipelines.
- # When merge request is matched, it selects MR pipelines.
- where(merge_request: [nil, merge_request], ref: ref, sha: sha)
- .sort_by_merge_request_pipelines
- end
+ scope :for_sha, -> (sha) { where(sha: sha) }
+ scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
+ scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) }
scope :triggered_by_merge_request, -> (merge_request) do
where(source: :merge_request_event, merge_request: merge_request)
end
- scope :detached_merge_request_pipelines, -> (merge_request) do
- triggered_by_merge_request(merge_request).where(target_sha: nil)
+ scope :detached_merge_request_pipelines, -> (merge_request, sha) do
+ triggered_by_merge_request(merge_request).for_sha(sha)
end
- scope :merge_request_pipelines, -> (merge_request) do
- triggered_by_merge_request(merge_request).where.not(target_sha: nil)
+ scope :merge_request_pipelines, -> (merge_request, source_sha) do
+ triggered_by_merge_request(merge_request).for_source_sha(source_sha)
end
scope :mergeable_merge_request_pipelines, -> (merge_request) do
triggered_by_merge_request(merge_request).where(target_sha: merge_request.target_branch_sha)
end
+ scope :triggered_for_branch, -> (ref) do
+ where(source: branch_pipeline_sources).where(ref: ref, tag: false)
+ end
+
# Returns the pipelines in descending order (= newest first), optionally
# limited to a number of references.
#
@@ -298,8 +297,8 @@ module Ci
sources.reject { |source| source == "external" }.values
end
- def self.latest_for_merge_request(merge_request, ref, sha)
- for_merge_request(merge_request, ref, sha).first
+ def self.branch_pipeline_sources
+ @branch_pipeline_sources ||= sources.reject { |source| source == 'merge_request_event' }.values
end
def self.ci_sources_values
diff --git a/app/models/label.rb b/app/models/label.rb
index 1c3db3eb35d..96bdb7f17c5 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -126,6 +126,13 @@ class Label < ActiveRecord::Base
fuzzy_search(query, [:title, :description])
end
+ # Override Gitlab::SQL::Pattern.min_chars_for_partial_matching as
+ # label queries are never global, and so will not use a trigram
+ # index. That means we can have just one character in the LIKE.
+ def self.min_chars_for_partial_matching
+ 1
+ end
+
def open_issues_count(user = nil)
issues_count(user, state: 'opened')
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index acf80addc6a..aeb6acf0ac0 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -203,6 +203,22 @@ class MergeRequest < ActiveRecord::Base
'!'
end
+ # Returns the top 100 target branches
+ #
+ # The returned value is a Array containing branch names
+ # sort by updated_at of merge request:
+ #
+ # ['master', 'develop', 'production']
+ #
+ # limit - The maximum number of target branch to return.
+ def self.recent_target_branches(limit: 100)
+ group(:target_branch)
+ .select(:target_branch)
+ .reorder('MAX(merge_requests.updated_at) DESC')
+ .limit(limit)
+ .pluck(:target_branch)
+ end
+
def rebase_in_progress?
strong_memoize(:rebase_in_progress) do
# The source project can be deleted
@@ -1133,12 +1149,18 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
end
- def all_pipelines(shas: all_commit_shas)
+ def all_pipelines
return Ci::Pipeline.none unless source_project
- @all_pipelines ||=
- source_project.ci_pipelines
- .for_merge_request(self, source_branch, all_commit_shas)
+ shas = all_commit_shas
+
+ strong_memoize(:all_pipelines) do
+ Ci::Pipeline.from_union(
+ [source_project.ci_pipelines.merge_request_pipelines(self, shas),
+ source_project.ci_pipelines.detached_merge_request_pipelines(self, shas),
+ source_project.ci_pipelines.triggered_for_branch(source_branch).for_sha(shas)],
+ remove_duplicates: false).sort_by_merge_request_pipelines
+ end
end
def update_head_pipeline
@@ -1373,8 +1395,7 @@ class MergeRequest < ActiveRecord::Base
private
def find_actual_head_pipeline
- source_project&.ci_pipelines
- &.latest_for_merge_request(self, source_branch, diff_head_sha)
+ all_pipelines.for_sha_or_source_sha(diff_head_sha).first
end
def source_project_variables
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 351a662ae83..e53e2c8fc43 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -286,13 +286,11 @@ class MergeRequestDiff < ActiveRecord::Base
return yield(@external_diff_file) if @external_diff_file
external_diff.open do |file|
- begin
- @external_diff_file = file
+ @external_diff_file = file
- yield(@external_diff_file)
- ensure
- @external_diff_file = nil
- end
+ yield(@external_diff_file)
+ ensure
+ @external_diff_file = nil
end
end
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index e8d936e265c..16ec4ed470f 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -23,6 +23,6 @@ class MergeRequestDiffFile < ActiveRecord::Base
super
end
- binary? ? content.unpack('m0').first : content
+ binary? ? content.unpack1('m0') : content
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index a5c479bdc0c..dea34e812ca 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -11,6 +11,7 @@ class Namespace < ApplicationRecord
include IgnorableColumn
include FeatureGate
include FromUnion
+ include Gitlab::Utils::StrongMemoize
ignore_column :deleted_at
@@ -267,6 +268,22 @@ class Namespace < ApplicationRecord
owner.refresh_authorized_projects
end
+ def auto_devops_enabled?
+ first_auto_devops_config[:status]
+ end
+
+ def first_auto_devops_config
+ return { scope: :group, status: auto_devops_enabled } unless auto_devops_enabled.nil?
+
+ strong_memoize(:first_auto_devops_config) do
+ if has_parent?
+ parent.first_auto_devops_config
+ else
+ { scope: :instance, status: Gitlab::CurrentSettings.auto_devops_enabled? }
+ end
+ end
+ end
+
private
def path_or_parent_changed?
diff --git a/app/models/note.rb b/app/models/note.rb
index 1578ae9c4cc..2c9980b1a0d 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -313,6 +313,14 @@ class Note < ActiveRecord::Base
!system?
end
+ # Since we're using `updated_at` as `last_edited_at`, it could be touched by transforming / resolving a note.
+ # This makes sure it is only marked as edited when the note body is updated.
+ def edited?
+ return false if updated_by.blank?
+
+ super
+ end
+
def cross_reference_not_visible_for?(user)
cross_reference? && !all_referenced_mentionables_allowed?(user)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 4cc13f372c1..59b139e5986 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -631,12 +631,21 @@ class Project < ActiveRecord::Base
end
def has_auto_devops_implicitly_enabled?
- auto_devops&.enabled.nil? &&
- (Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
+ auto_devops_config = first_auto_devops_config
+
+ auto_devops_config[:scope] != :project && auto_devops_config[:status]
end
def has_auto_devops_implicitly_disabled?
- auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
+ auto_devops_config = first_auto_devops_config
+
+ auto_devops_config[:scope] != :project && !auto_devops_config[:status]
+ end
+
+ def first_auto_devops_config
+ return namespace.first_auto_devops_config if auto_devops&.enabled.nil?
+
+ { scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) }
end
def daily_statistics_enabled?
@@ -1200,11 +1209,9 @@ class Project < ActiveRecord::Base
def repo_exists?
strong_memoize(:repo_exists) do
- begin
- repository.exists?
- rescue
- false
- end
+ repository.exists?
+ rescue
+ false
end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index f7064d5aaea..81302c516c2 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -205,12 +205,10 @@ class JiraService < IssueTrackerService
# if any transition fails it will log the error message and stop the transition sequence
def transition_issue(issue)
jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).each do |transition_id|
- begin
- issue.transitions.build.save!(transition: { id: transition_id })
- rescue => error
- log_error("Issue transition failed", error: error.message, client_url: client_url)
- return false
- end
+ issue.transitions.build.save!(transition: { id: transition_id })
+ rescue => error
+ log_error("Issue transition failed", error: error.message, client_url: client_url)
+ return false
end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 851175a61b7..ff355295862 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -265,16 +265,14 @@ class Repository
# to avoid unnecessary syncing.
def keep_around(*shas)
shas.each do |sha|
- begin
- next unless sha.present? && commit_by(oid: sha)
+ next unless sha.present? && commit_by(oid: sha)
- next if kept_around?(sha)
+ next if kept_around?(sha)
- # This will still fail if the file is corrupted (e.g. 0 bytes)
- raw_repository.write_ref(keep_around_ref_name(sha), sha)
- rescue Gitlab::Git::CommandError => ex
- Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}"
- end
+ # This will still fail if the file is corrupted (e.g. 0 bytes)
+ raw_repository.write_ref(keep_around_ref_name(sha), sha)
+ rescue Gitlab::Git::CommandError => ex
+ Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}"
end
end
@@ -534,10 +532,9 @@ class Repository
end
def root_ref
- # When the repo does not exist, or there is no root ref, we raise this error so no data is cached.
- raw_repository&.root_ref or raise Gitlab::Git::Repository::NoRepository # rubocop:disable Style/AndOr
+ raw_repository&.root_ref
end
- cache_method :root_ref
+ cache_method_asymmetrically :root_ref
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/314
def exists?
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
index ae6778e49be..5fc59b274f5 100644
--- a/app/models/user_interacted_project.rb
+++ b/app/models/user_interacted_project.rb
@@ -26,16 +26,14 @@ class UserInteractedProject < ActiveRecord::Base
cached_exists?(attributes) do
transaction(requires_new: true) do
- begin
- where(attributes).select(1).first || create!(attributes)
- true # not caching the whole record here for now
- rescue ActiveRecord::RecordNotUnique
- # Note, above queries are not atomic and prone
- # to race conditions (similar like #find_or_create!).
- # In the case where we hit this, the record we want
- # already exists - shortcut and return.
- true
- end
+ where(attributes).select(1).first || create!(attributes)
+ true # not caching the whole record here for now
+ rescue ActiveRecord::RecordNotUnique
+ # Note, above queries are not atomic and prone
+ # to race conditions (similar like #find_or_create!).
+ # In the case where we hit this, the record we want
+ # already exists - shortcut and return.
+ true
end
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index e74e5f008d7..db49d3bed9c 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -26,7 +26,7 @@ class GroupPolicy < BasePolicy
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
condition(:has_projects) do
- GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any?
+ GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true, only_owned: true }).execute.any?
end
condition(:has_clusters, scope: :subject) { clusterable_has_clusters? }
@@ -55,6 +55,7 @@ class GroupPolicy < BasePolicy
rule { has_projects }.policy do
enable :read_list
enable :read_label
+ enable :read_group
end
rule { has_access }.enable :read_namespace
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 000b7c433a2..161eebcfb3f 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -42,11 +42,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
def empty_repo_statistics_anchors
[
- license_anchor_data,
- commits_anchor_data,
- branches_anchor_data,
- tags_anchor_data,
- files_anchor_data
+ license_anchor_data
].compact.select { |item| item.is_link }
end
@@ -55,9 +51,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
new_file_anchor_data,
readme_anchor_data,
changelog_anchor_data,
- contribution_guide_anchor_data,
- autodevops_anchor_data,
- kubernetes_cluster_anchor_data
+ contribution_guide_anchor_data
].compact.reject { |item| item.is_link }
end
diff --git a/app/serializers/detailed_status_entity.rb b/app/serializers/detailed_status_entity.rb
index da994d78286..4f23ef0ed82 100644
--- a/app/serializers/detailed_status_entity.rb
+++ b/app/serializers/detailed_status_entity.rb
@@ -9,16 +9,14 @@ class DetailedStatusEntity < Grape::Entity
expose :details_path
expose :illustration do |status|
- begin
- illustration = {
- image: ActionController::Base.helpers.image_path(status.illustration[:image])
- }
- illustration = status.illustration.merge(illustration)
+ illustration = {
+ image: ActionController::Base.helpers.image_path(status.illustration[:image])
+ }
+ illustration = status.illustration.merge(illustration)
- illustration
- rescue NotImplementedError
- # ignored
- end
+ illustration
+ rescue NotImplementedError
+ # ignored
end
expose :favicon do |status|
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 5ac1e590d39..fba72410217 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -28,7 +28,8 @@ class PipelineEntity < Grape::Entity
expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable
expose :failure_reason?, as: :failure_reason
- expose :detached_merge_request_pipeline?, as: :detached
+ expose :detached_merge_request_pipeline?, as: :detached_merge_request_pipeline
+ expose :merge_request_pipeline?, as: :merge_request_pipeline
end
expose :details do
diff --git a/app/services/groups/auto_devops_service.rb b/app/services/groups/auto_devops_service.rb
new file mode 100644
index 00000000000..1925e0cc0ea
--- /dev/null
+++ b/app/services/groups/auto_devops_service.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Groups
+ class AutoDevopsService < Groups::BaseService
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_group, group)
+
+ group.update(auto_devops_enabled: auto_devops_enabled)
+ end
+
+ private
+
+ def auto_devops_enabled
+ params[:auto_devops_enabled]
+ end
+ end
+end
diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
index 7998976b00a..a9570176e81 100644
--- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
@@ -42,17 +42,15 @@ module Projects
def parse_response_links(objects_response)
objects_response.each_with_object([]) do |entry, link_list|
- begin
- link = entry.dig('actions', DOWNLOAD_ACTION, 'href')
+ link = entry.dig('actions', DOWNLOAD_ACTION, 'href')
- raise DownloadLinkNotFound unless link
+ raise DownloadLinkNotFound unless link
- link_list << LfsDownloadObject.new(oid: entry['oid'],
- size: entry['size'],
- link: add_credentials(link))
- rescue DownloadLinkNotFound, Addressable::URI::InvalidURIError
- log_error("Link for Lfs Object with oid #{entry['oid']} not found or invalid.")
- end
+ link_list << LfsDownloadObject.new(oid: entry['oid'],
+ size: entry['size'],
+ link: add_credentials(link))
+ rescue DownloadLinkNotFound, Addressable::URI::InvalidURIError
+ log_error("Link for Lfs Object with oid #{entry['oid']} not found or invalid.")
end
end
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index 398f00a598d..a009f479d5d 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -75,17 +75,15 @@ module Projects
create_tmp_storage_dir
File.open(tmp_filename, 'wb') do |file|
- begin
- yield file
- rescue StandardError => e
- # If the lfs file is successfully downloaded it will be removed
- # when it is added to the project's lfs files.
- # Nevertheless if any excetion raises the file would remain
- # in the file system. Here we ensure to remove it
- File.unlink(file) if File.exist?(file)
-
- raise e
- end
+ yield file
+ rescue StandardError => e
+ # If the lfs file is successfully downloaded it will be removed
+ # when it is added to the project's lfs files.
+ # Nevertheless if any excetion raises the file would remain
+ # in the file system. Here we ensure to remove it
+ File.unlink(file) if File.exist?(file)
+
+ raise e
end
end
diff --git a/app/validators/sha_validator.rb b/app/validators/sha_validator.rb
index 085fca4d65d..77e7cfa4f6b 100644
--- a/app/validators/sha_validator.rb
+++ b/app/validators/sha_validator.rb
@@ -2,7 +2,7 @@
class ShaValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- return if value.blank? || value.match(/\A\h{40}\z/)
+ return if value.blank? || Commit.valid_hash?(value)
record.errors.add(attribute, 'is not a valid SHA')
end
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 39c0c113793..dbd01c3c61a 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -13,7 +13,7 @@
= visibility_level_icon(@group.visibility_level, fw: false, options: {class: 'icon'})
.home-panel-metadata.d-flex.align-items-center.text-secondary
%span
- = _("Group")
+ = _("Group ID: %{group_id}") % { group_id: @group.id }
- if current_user
%span.access-request-links.prepend-left-8
= render 'shared/members/access_request_links', source: @group
diff --git a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
new file mode 100644
index 00000000000..e7efc0237c8
--- /dev/null
+++ b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
@@ -0,0 +1,15 @@
+= form_for group, url: update_auto_devops_group_settings_ci_cd_path(group), method: :patch do |f|
+ = form_errors(group)
+ %fieldset
+ .form-group
+ .card.auto-devops-card
+ .card-body
+ .form-check
+ = f.check_box :auto_devops_enabled, class: 'form-check-input', checked: group.auto_devops_enabled?
+ = f.label :auto_devops_enabled, class: 'form-check-label' do
+ %strong= s_('GroupSettings|Default to Auto DevOps pipeline for all projects within this group')
+ %span.badge.badge-info#auto-devops-badge= badge_for_auto_devops_scope(group)
+ .form-text.text-muted
+ = s_('GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found.')
+ = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank'
+ = f.submit _('Save changes'), class: 'btn btn-success prepend-top-15'
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index d9332e36ef5..d0f5cd94002 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -19,3 +19,17 @@
= _('Register and see your runners for this group.')
.settings-content
= render 'groups/runners/index'
+
+%section.settings#auto-devops-settings.no-animate{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Auto DevOps')
+ %button.btn.btn-default.js-settings-toggle{ type: "button" }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ - auto_devops_url = help_page_path('topics/autodevops/index')
+ - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
+ = s_('GroupSettings|Auto DevOps will automatically build, test and deploy your application based on a predefined Continuous Integration and Delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end}').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe }
+
+ .settings-content
+ = render 'groups/settings/ci_cd/auto_devops_form', group: @group
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 5a66b02c048..438340464bd 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -38,4 +38,4 @@
%li= link_to _('New project'), new_project_path, class: 'qa-global-new-project-link'
- if current_user.can_create_group?
%li= link_to _('New group'), new_group_path
- %li= link_to _('New snippet'), new_snippet_path
+ %li= link_to _('New snippet'), new_snippet_path, class: 'qa-global-new-snippet-link'
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index f659c89dd30..1ec368f8910 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -29,7 +29,7 @@
- if dashboard_nav_link?(:snippets)
= nav_link(controller: 'dashboard/snippets', html_options: { class: ["d-none d-xl-block", ("d-lg-block" unless has_extra_nav_icons?)] }) do
- = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: _('Snippets') do
+ = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets qa-snippets-link', title: _('Snippets') do
= _('Snippets')
- if any_dashboard_nav_link?([:groups, :milestones, :activity, :snippets])
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 2fdd65f639b..bb2d206ba91 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -220,7 +220,7 @@
= _('Repository')
- if template_exists?('admin/application_settings/templates')
= nav_link(path: 'application_settings#templates') do
- = link_to templates_admin_application_settings_path, title: _('Templates') do
+ = link_to templates_admin_application_settings_path, title: _('Templates'), class: 'qa-admin-settings-template-item' do
%span
= _('Templates')
= nav_link(path: 'application_settings#ci_cd') do
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 21ea9f3b2f3..eefe86eb6b4 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -20,13 +20,14 @@
= _('Overview')
%ul.sidebar-sub-level-items
- = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: ['groups#show', 'groups#details', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do
= link_to group_path(@group) do
%strong.fly-out-top-item-name
= _('Overview')
%li.divider.fly-out-top-item
- = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
- = link_to group_path(@group), title: _('Group details') do
+
+ = nav_link(path: ['groups#show', 'groups#details', 'groups#subgroups'], html_options: { class: 'home' }) do
+ = link_to details_group_path(@group), title: _('Group details') do
%span
= _('Details')
@@ -40,9 +41,9 @@
- if group_sidebar_link?(:contribution_analytics)
= nav_link(path: 'analytics#show') do
- = link_to group_analytics_path(@group), title: 'Contribution Analytics', data: {placement: 'right'} do
+ = link_to group_analytics_path(@group), title: _('Contribution Analytics'), data: { placement: 'right' } do
%span
- Contribution Analytics
+ = _('Contribution Analytics')
= render_if_exists "layouts/nav/ee/epic_link", group: @group
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index 7a5fff96676..b72f0e39b23 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -5,4 +5,5 @@
- if current_user && can?(current_user, :download_code, project)
= render 'shared/no_ssh'
= render 'shared/no_password'
- = render 'shared/auto_devops_implicitly_enabled_banner', project: project
+ - unless project.empty_repo?
+ = render 'shared/auto_devops_implicitly_enabled_banner', project: project
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 1d7287410ea..4ac5a74c85c 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -57,7 +57,10 @@
- if can?(current_user, :download_code, @project)
%nav.project-stats
.nav-links.quick-links
- = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
+ - if @project.empty_repo?
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
+ - else
+ = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
.home-panel-home-desc.mt-1
- if @project.description.present?
diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml
index 4bef45932d0..88fa31a73b0 100644
--- a/app/views/projects/blob/_header_content.html.haml
+++ b/app/views/projects/blob/_header_content.html.haml
@@ -1,7 +1,7 @@
.file-header-content
= blob_icon blob.mode, blob.name
- %strong.file-title-name
+ %strong.file-title-name.qa-file-title-name
= blob.name
= copy_file_path_button(blob.path)
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 081990ac9b7..9fa31c147eb 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -7,89 +7,64 @@
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
= render "home_panel"
- .project-empty-note-panel
- %h4.append-bottom-20
- = _('The repository for this project is empty')
+ %h4.prepend-top-0.append-bottom-8
+ = _('The repository for this project is empty')
- - if @project.can_current_user_push_code?
- %p
- - link_to_cli = link_to _('command line instructions'), '#repo-command-line-instructions'
- = _('If you already have files you can push them using the %{link_to_cli} below.').html_safe % { link_to_cli: link_to_cli }
- %p
- %em
- - link_to_protected_branches = link_to _('Learn more about protected branches'), help_page_path('user/project/protected_branches')
- = _('Note that the master branch is automatically protected. %{link_to_protected_branches}').html_safe % { link_to_protected_branches: link_to_protected_branches }
-
- %hr
- %p
- - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
- - link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
- = s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
+ - if @project.can_current_user_push_code?
+ %p.append-bottom-0
+ = _('You can create files directly in GitLab using one of the following options.')
- %hr
- %p
- = _('Otherwise it is recommended you start with one of the options below.')
- .prepend-top-20
-
- %nav.project-buttons
- .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller.qa-quick-actions
- .fade-left= icon('angle-left')
- .fade-right= icon('angle-right')
- .nav-links.scrolling-tabs.quick-links
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
+ .project-buttons.qa-quick-actions
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons
- if can?(current_user, :push_code, @project)
- %div
- .prepend-top-20
- .empty_wrapper
- %h3#repo-command-line-instructions.page-title-empty
- = _('Command line instructions')
- .git-empty.js-git-empty
- %fieldset
- %h5= _('Git global setup')
- %pre.bg-light
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
-
- %fieldset
- %h5= _('Create a new repository')
- %pre.bg-light
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- cd #{h @project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin master
+ .empty-wrapper.prepend-top-32
+ %h3#repo-command-line-instructions.page-title-empty
+ = _('Command line instructions')
+ %p
+ = _('You can also upload existing files from your computer using the instructions below.')
+ .git-empty.js-git-empty
+ %fieldset
+ %h5= _('Git global setup')
+ %pre.bg-light
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5= _('Existing folder')
- %pre.bg-light
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- git add .
- git commit -m "Initial commit"
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin master
+ %fieldset
+ %h5= _('Create a new repository')
+ %pre.bg-light
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
- %fieldset
- %h5= _('Existing Git repository')
- %pre.bg-light
- :preserve
- cd existing_repo
- git remote rename origin old-origin
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
- - if @project.can_current_user_push_to_default_branch?
- %span><
- git push -u origin --all
- git push -u origin --tags
+ %fieldset
+ %h5= _('Push an existing folder')
+ %pre.bg-light
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ git add .
+ git commit -m "Initial commit"
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin master
- - if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to _('Remove project'), [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove float-right"
+ %fieldset
+ %h5= _('Push an existing Git repository')
+ %pre.bg-light
+ :preserve
+ cd existing_repo
+ git remote rename origin old-origin
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')}
+ - if @project.can_current_user_push_to_default_branch?
+ %span><
+ git push -u origin --all
+ git push -u origin --tags
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 70011d58c8a..92e34b3ceda 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -3,7 +3,7 @@
- if @merge_request.closed_without_fork?
.alert.alert-danger
- %p The source project of this merge request has been removed.
+ The source project of this merge request has been removed.
.detail-page-header
.detail-page-header-body
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 8c4d1c32ebe..fac68a36e79 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -8,15 +8,15 @@
.card.auto-devops-card
.card-body
.form-check
- = form.check_box :enabled, class: 'form-check-input js-toggle-extra-settings', checked: @project.auto_devops_enabled?
+ = form.check_box :enabled, class: 'form-check-input js-toggle-extra-settings', checked: auto_devops_enabled
= form.label :enabled, class: 'form-check-label' do
%strong= s_('CICD|Default to Auto DevOps pipeline')
- - if @project.has_auto_devops_implicitly_enabled?
- %span.badge.badge-info.js-instance-default-badge= s_('CICD|instance enabled')
+ - if auto_devops_enabled
+ %span.badge.badge-info.js-instance-default-badge= badge_for_auto_devops_scope(@project)
.form-text.text-muted
= s_('CICD|The Auto DevOps pipeline will run if no alternative CI configuration file is found.')
= link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank'
- .card-footer.js-extra-settings{ class: @project.auto_devops_enabled? || 'hidden' }
+ .card-footer.js-extra-settings{ class: auto_devops_enabled || 'hidden' }
%p.settings-message.text-center
- kubernetes_cluster_link = help_page_path('user/project/clusters/index')
- kubernetes_cluster_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: kubernetes_cluster_link }
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 6966bf96724..548b7c06867 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -26,7 +26,7 @@
= s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
= link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
.settings-content
- = render 'autodevops_form'
+ = render 'autodevops_form', auto_devops_enabled: @project.auto_devops_enabled?
= render_if_exists 'projects/settings/ci_cd/protected_environments', expanded: expanded
diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml
index 6b15331db01..451a79becc3 100644
--- a/app/views/projects/settings/operations/_error_tracking.html.haml
+++ b/app/views/projects/settings/operations/_error_tracking.html.haml
@@ -2,7 +2,7 @@
- setting = error_tracking_setting
-%section.settings.expanded.border-0.no-animate
+%section.settings.expanded.no-animate
.settings-header
%h4
= _('Error Tracking')
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index 2822debe426..6f777305a54 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -2,5 +2,6 @@
- page_title _('Operations Settings')
- breadcrumb_title _('Operations Settings')
+= render_if_exists 'projects/settings/operations/incidents'
= render 'projects/settings/operations/error_tracking', expanded: true
= render_if_exists 'projects/settings/operations/tracing'
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index 5073e6ad48f..d7e57fc0d01 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -1,4 +1,4 @@
-.file-content.code.js-syntax-highlight
+.file-content.code.js-syntax-highlight.qa-file-content
.line-numbers
- if blob.data.present?
- link_icon = icon('link')
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index f43be304e6b..2bcfcb6fa7c 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -137,6 +137,11 @@
%li.filter-dropdown-item{ data: { value: 'no', capitalize: true } }
%button.btn.btn-link{ type: 'button' }
= _('No')
+ #js-dropdown-target-branch.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link.js-data-value.monospace
+ {{title}}
= render_if_exists 'shared/issuable/filter_weight', type: type
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 41d6ae79c81..6fec435cc87 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -36,14 +36,13 @@
= user_status(note.author)
%span.note-headline-light
= note.author.to_reference
- %span.note-headline-light
- %span.note-headline-meta
- - if note.system
- %span.system-note-message
- = markdown_field(note, :note)
- %span.system-note-separator
- &middot;
- %a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
+ %span.note-headline-light.note-headline-meta
+ - if note.system
+ %span.system-note-message
+ = markdown_field(note, :note)
+ %span.system-note-separator
+ &middot;
+ %a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
.note-actions
- if note.for_personal_snippet?
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 3007da0c189..6f2ddc5bdba 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -9,7 +9,7 @@
.form-group.row
= f.label :title, class: 'col-form-label col-sm-2'
.col-sm-10
- = f.text_field :title, class: 'form-control', required: true, autofocus: true
+ = f.text_field :title, class: 'form-control qa-snippet-title', required: true, autofocus: true
= render 'shared/form_elements/description', model: @snippet, project: @project, form: f
@@ -21,7 +21,7 @@
.col-sm-10
.file-holder.snippet
.js-file-title.file-title
- = f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name'
+ = f.text_field :file_name, placeholder: "Optionally name this file to add code highlighting, e.g. example.rb for Ruby.", class: 'form-control snippet-file-name qa-snippet-file-name'
.file-content.code
%pre#editor= @snippet.content
= f.hidden_field :content, class: 'snippet-file-content'
@@ -31,7 +31,7 @@
.form-actions
- if @snippet.new_record?
- = f.submit 'Create snippet', class: "btn-success btn"
+ = f.submit 'Create snippet', class: "btn-success btn qa-create-snippet-button"
- else
= f.submit 'Save changes', class: "btn-success btn"
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index a43296aa806..0c07eae8643 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -1,6 +1,6 @@
.detail-page-header
.detail-page-header-body
- .snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
+ .snippet-box.qa-snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
%span.sr-only
= visibility_level_label(@snippet.visibility_level)
= visibility_level_icon(@snippet.visibility_level, fw: false)
@@ -17,11 +17,11 @@
= render "snippets/actions"
.snippet-header.limited-header-width
- %h2.snippet-title.prepend-top-0.append-bottom-0
+ %h2.snippet-title.prepend-top-0.append-bottom-0.qa-snippet-title
= markdown_field(@snippet, :title)
- if @snippet.description.present?
- .description
+ .description.qa-snippet-description
.wiki
= markdown_field(@snippet, :description)
%textarea.hidden.js-task-list-field
@@ -34,7 +34,7 @@
.embed-snippet
.input-group
.input-group-prepend
- %button.btn.btn-svg.embed-toggle.input-group-text{ 'data-toggle': 'dropdown', type: 'button' }
+ %button.btn.btn-svg.embed-toggle.input-group-text.qa-embed-type{ 'data-toggle': 'dropdown', type: 'button' }
%span.js-embed-action= _("Embed")
= sprite_icon('angle-down', size: 12, css_class: 'caret-down')
%ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list
diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb
index 27b94a82444..17946bbc5ca 100644
--- a/app/workers/concerns/waitable_worker.rb
+++ b/app/workers/concerns/waitable_worker.rb
@@ -25,11 +25,9 @@ module WaitableWorker
failed = []
args_list.each do |args|
- begin
- new.perform(*args)
- rescue
- failed << args
- end
+ new.perform(*args)
+ rescue
+ failed << args
end
bulk_perform_async(failed) if failed.present?
diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb
index 49c7a403838..2827529cc1c 100644
--- a/app/workers/create_gpg_signature_worker.rb
+++ b/app/workers/create_gpg_signature_worker.rb
@@ -20,11 +20,9 @@ class CreateGpgSignatureWorker
# This calculates and caches the signature in the database
commits.each do |commit|
- begin
- Gitlab::Gpg::Commit.new(commit).signature
- rescue => e
- Rails.logger.error("Failed to create signature for commit #{commit.id}. Error: #{e.message}")
- end
+ Gitlab::Gpg::Commit.new(commit).signature
+ rescue => e
+ Rails.logger.error("Failed to create signature for commit #{commit.id}. Error: #{e.message}")
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 17ad1d5ab88..ed3e354e4c2 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -52,24 +52,22 @@ class EmailsOnPushWorker
end
valid_recipients(recipients).each do |recipient|
- begin
- send_email(
- recipient,
- project_id,
- author_id: author_id,
- ref: ref,
- action: action,
- compare: compare,
- reverse_compare: reverse_compare,
- diff_refs: diff_refs,
- send_from_committer_email: send_from_committer_email,
- disable_diffs: disable_diffs
- )
-
- # These are input errors and won't be corrected even if Sidekiq retries
- rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
- logger.info("Failed to send e-mail for project '#{project.full_name}' to #{recipient}: #{e}")
- end
+ send_email(
+ recipient,
+ project_id,
+ author_id: author_id,
+ ref: ref,
+ action: action,
+ compare: compare,
+ reverse_compare: reverse_compare,
+ diff_refs: diff_refs,
+ send_from_committer_email: send_from_committer_email,
+ disable_diffs: disable_diffs
+ )
+
+ # These are input errors and won't be corrected even if Sidekiq retries
+ rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
+ logger.info("Failed to send e-mail for project '#{project.full_name}' to #{recipient}: #{e}")
end
ensure
@email = nil
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index fe5d27b087d..206eb71b898 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -126,11 +126,9 @@ module ObjectStorage
def process_uploader(uploader)
MigrationResult.new(uploader.upload).tap do |result|
- begin
- uploader.migrate!(@to_store)
- rescue => e
- result.error = e
- end
+ uploader.migrate!(@to_store)
+ rescue => e
+ result.error = e
end
end
end
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index ac4e9710f33..02a69ea3b54 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -8,16 +8,15 @@ class PipelineScheduleWorker
def perform
Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
.preload(:owner, :project).find_each do |schedule|
- begin
- Ci::CreatePipelineService.new(schedule.project,
- schedule.owner,
- ref: schedule.ref)
- .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule)
- rescue => e
- error(schedule, e)
- ensure
- schedule.schedule_next_run!
- end
+
+ Ci::CreatePipelineService.new(schedule.project,
+ schedule.owner,
+ ref: schedule.ref)
+ .execute!(:schedule, ignore_skip_ci: true, save_on_errors: true, schedule: schedule)
+ rescue => e
+ error(schedule, e)
+ ensure
+ schedule.schedule_next_run!
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index 41913900571..3497a1f9280 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -6,11 +6,9 @@ class RemoveExpiredMembersWorker
def perform
Member.expired.find_each do |member|
- begin
- Members::DestroyService.new.execute(member, skip_authorization: true)
- rescue => ex
- logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
- end
+ Members::DestroyService.new.execute(member, skip_authorization: true)
+ rescue => ex
+ logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
end
end
end
diff --git a/changelogs/unreleased/10029-env-item.yml b/changelogs/unreleased/10029-env-item.yml
new file mode 100644
index 00000000000..f4e742d3e17
--- /dev/null
+++ b/changelogs/unreleased/10029-env-item.yml
@@ -0,0 +1,5 @@
+---
+title: Removes EE differences for environment_item.vue
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/10081-env-table.yml b/changelogs/unreleased/10081-env-table.yml
new file mode 100644
index 00000000000..b27a1be8cca
--- /dev/null
+++ b/changelogs/unreleased/10081-env-table.yml
@@ -0,0 +1,5 @@
+---
+title: Removes EE differences for environments_table.vue
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/25942-remove-fake-repository-path-response.yml b/changelogs/unreleased/25942-remove-fake-repository-path-response.yml
new file mode 100644
index 00000000000..e1da28ab03c
--- /dev/null
+++ b/changelogs/unreleased/25942-remove-fake-repository-path-response.yml
@@ -0,0 +1,5 @@
+---
+title: Remove fake repository_path response
+merge_request: 25942
+author: Fabio Papa
+type: other
diff --git a/changelogs/unreleased/48324-enable-squash-message-on-fast-forward.yml b/changelogs/unreleased/48324-enable-squash-message-on-fast-forward.yml
new file mode 100644
index 00000000000..789ff4f9f89
--- /dev/null
+++ b/changelogs/unreleased/48324-enable-squash-message-on-fast-forward.yml
@@ -0,0 +1,5 @@
+---
+title: Allow modifying squash commit message for fast-forward only merge method
+merge_request: 26017
+author:
+type: fixed
diff --git a/changelogs/unreleased/49863-ingress-ip-loading-state.yml b/changelogs/unreleased/49863-ingress-ip-loading-state.yml
new file mode 100644
index 00000000000..51bb27d3153
--- /dev/null
+++ b/changelogs/unreleased/49863-ingress-ip-loading-state.yml
@@ -0,0 +1,5 @@
+---
+title: Show loading spinner while Ingress/Knative IP is being assigned
+merge_request: 25912
+author:
+type: changed
diff --git a/changelogs/unreleased/52447-auto-devops-at-group-level.yml b/changelogs/unreleased/52447-auto-devops-at-group-level.yml
new file mode 100644
index 00000000000..0a21c6a2b7b
--- /dev/null
+++ b/changelogs/unreleased/52447-auto-devops-at-group-level.yml
@@ -0,0 +1,5 @@
+---
+title: Enable/disable Auto DevOps at the Group level
+merge_request: 25533
+author:
+type: added
diff --git a/changelogs/unreleased/56833-project-improve-empty-repository-state-ui-fe.yml b/changelogs/unreleased/56833-project-improve-empty-repository-state-ui-fe.yml
new file mode 100644
index 00000000000..19cf3d69db1
--- /dev/null
+++ b/changelogs/unreleased/56833-project-improve-empty-repository-state-ui-fe.yml
@@ -0,0 +1,5 @@
+---
+title: 'Project: Improve empty repository state UI'
+merge_request: 26024
+author:
+type: other
diff --git a/changelogs/unreleased/56970-fix-mr-stuck-loading-on-error.yml b/changelogs/unreleased/56970-fix-mr-stuck-loading-on-error.yml
new file mode 100644
index 00000000000..f86c77d0e24
--- /dev/null
+++ b/changelogs/unreleased/56970-fix-mr-stuck-loading-on-error.yml
@@ -0,0 +1,5 @@
+---
+title: Disable timeout on merge request merging poll
+merge_request: 25988
+author:
+type: fixed
diff --git a/changelogs/unreleased/57330-fix-comment-edited.yml b/changelogs/unreleased/57330-fix-comment-edited.yml
new file mode 100644
index 00000000000..68cf6c03d4c
--- /dev/null
+++ b/changelogs/unreleased/57330-fix-comment-edited.yml
@@ -0,0 +1,5 @@
+---
+title: Fix notes being marked as edited after resolving
+merge_request: 26143
+author:
+type: fixed
diff --git a/changelogs/unreleased/57579-gitlab-project-import-fails-sidekiq-undefined-method-import_jid.yml b/changelogs/unreleased/57579-gitlab-project-import-fails-sidekiq-undefined-method-import_jid.yml
deleted file mode 100644
index f7d6a6c4863..00000000000
--- a/changelogs/unreleased/57579-gitlab-project-import-fails-sidekiq-undefined-method-import_jid.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix import_jid error on project import
-merge_request: 25239
-author:
-type: fixed
diff --git a/changelogs/unreleased/58149-fix-read-list-board-policy.yml b/changelogs/unreleased/58149-fix-read-list-board-policy.yml
deleted file mode 100644
index 964813f4c9a..00000000000
--- a/changelogs/unreleased/58149-fix-read-list-board-policy.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fix error when viewing group issue boards when user doesn't have explicit group
- permissions
-merge_request: 25524
-author:
-type: fixed
diff --git a/changelogs/unreleased/58208-explicitly-set-masterauth.yml b/changelogs/unreleased/58208-explicitly-set-masterauth.yml
new file mode 100644
index 00000000000..e3512d11113
--- /dev/null
+++ b/changelogs/unreleased/58208-explicitly-set-masterauth.yml
@@ -0,0 +1,6 @@
+---
+title: Explicitly set master_auth setting to enable basic auth and client certificate
+ for new GKE clusters
+merge_request: 26018
+author:
+type: other
diff --git a/changelogs/unreleased/58739-hashed-storage-prevent-a-migration-and-rollback-running-at-the-same-time.yml b/changelogs/unreleased/58739-hashed-storage-prevent-a-migration-and-rollback-running-at-the-same-time.yml
new file mode 100644
index 00000000000..765a991bb6a
--- /dev/null
+++ b/changelogs/unreleased/58739-hashed-storage-prevent-a-migration-and-rollback-running-at-the-same-time.yml
@@ -0,0 +1,5 @@
+---
+title: 'Hashed Storage: Prevent a migration and rollback running at the same time'
+merge_request: 25976
+author:
+type: changed
diff --git a/changelogs/unreleased/58781-silent-progress-in-auto-devops.yml b/changelogs/unreleased/58781-silent-progress-in-auto-devops.yml
new file mode 100644
index 00000000000..e45db8eafc3
--- /dev/null
+++ b/changelogs/unreleased/58781-silent-progress-in-auto-devops.yml
@@ -0,0 +1,5 @@
+---
+title: Use curl silent/show-error options on Auto DevOps
+merge_request: 25954
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml b/changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml
new file mode 100644
index 00000000000..ebfb7aeaa1f
--- /dev/null
+++ b/changelogs/unreleased/58789-some-system-notes-on-issuable-are-folded-on-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Keep inline as much as possible in system notes on issuable
+merge_request: 25968
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml b/changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml
new file mode 100644
index 00000000000..e30f48ed1a8
--- /dev/null
+++ b/changelogs/unreleased/58797-broken-ui-on-a-closed-merge-request-from-a-deleted-source-project.yml
@@ -0,0 +1,5 @@
+---
+title: Fix UI for closed MR when source project is removed
+merge_request: 25967
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/allow-filtering-labels-by-a-single-character.yml b/changelogs/unreleased/allow-filtering-labels-by-a-single-character.yml
new file mode 100644
index 00000000000..31165bbadb7
--- /dev/null
+++ b/changelogs/unreleased/allow-filtering-labels-by-a-single-character.yml
@@ -0,0 +1,5 @@
+---
+title: Allow filtering labels list by one or two characters
+merge_request: 26012
+author:
+type: changed
diff --git a/changelogs/unreleased/expose-group-id-on-home-panel.yml b/changelogs/unreleased/expose-group-id-on-home-panel.yml
new file mode 100644
index 00000000000..1efe15a6e1a
--- /dev/null
+++ b/changelogs/unreleased/expose-group-id-on-home-panel.yml
@@ -0,0 +1,5 @@
+---
+title: Expose group id on home panel
+merge_request: 25897
+author: Peter Marko
+type: added
diff --git a/changelogs/unreleased/filter-merge-requests-by-target-branch.yml b/changelogs/unreleased/filter-merge-requests-by-target-branch.yml
new file mode 100644
index 00000000000..d0aba631c96
--- /dev/null
+++ b/changelogs/unreleased/filter-merge-requests-by-target-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Add target branch filter to merge requests search bar
+merge_request: 24380
+author: Hiroyuki Sato
+type: added
diff --git a/changelogs/unreleased/fix-ide-web-worker-relative-url.yml b/changelogs/unreleased/fix-ide-web-worker-relative-url.yml
new file mode 100644
index 00000000000..2accad68c4e
--- /dev/null
+++ b/changelogs/unreleased/fix-ide-web-worker-relative-url.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed Web IDE web workers not working with relative URLs
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml b/changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml
new file mode 100644
index 00000000000..dadbd5c940f
--- /dev/null
+++ b/changelogs/unreleased/fix-new-merge-request-diff-headers-sticky-position.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed sticky headers in merge request creation diffs
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-pipeline-entity.yml b/changelogs/unreleased/fix-pipeline-entity.yml
new file mode 100644
index 00000000000..b429139402c
--- /dev/null
+++ b/changelogs/unreleased/fix-pipeline-entity.yml
@@ -0,0 +1,5 @@
+---
+title: Add merge request pipeline flag to pipeline entity
+merge_request: 25846
+author:
+type: added
diff --git a/changelogs/unreleased/fix-review-app-env-url.yml b/changelogs/unreleased/fix-review-app-env-url.yml
new file mode 100644
index 00000000000..963cd0c2992
--- /dev/null
+++ b/changelogs/unreleased/fix-review-app-env-url.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes long review app subdomains
+merge_request: 25990
+author: walkafwalka
+type: fixed
diff --git a/changelogs/unreleased/fj-58804-fix-bitbucket-import.yml b/changelogs/unreleased/fj-58804-fix-bitbucket-import.yml
new file mode 100644
index 00000000000..dc44c64a055
--- /dev/null
+++ b/changelogs/unreleased/fj-58804-fix-bitbucket-import.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug in BitBucket imports with SHA shorter than 40 chars
+merge_request: 26050
+author:
+type: fixed
diff --git a/changelogs/unreleased/jc-fix-set-project-writable.yml b/changelogs/unreleased/jc-fix-set-project-writable.yml
deleted file mode 100644
index 0bfd90c3967..00000000000
--- a/changelogs/unreleased/jc-fix-set-project-writable.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix method to mark a project repository as writable
-merge_request: 25546
-author:
-type: fixed
diff --git a/changelogs/unreleased/more-pgroup-fix.yml b/changelogs/unreleased/more-pgroup-fix.yml
new file mode 100644
index 00000000000..6e85205ccba
--- /dev/null
+++ b/changelogs/unreleased/more-pgroup-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the last-ditch memory killer pgroup SIGKILL
+merge_request: 25940
+author:
+type: fixed
diff --git a/changelogs/unreleased/pravi-gitlab-ce-update-recaptcha.yml b/changelogs/unreleased/pravi-gitlab-ce-update-recaptcha.yml
new file mode 100644
index 00000000000..95379fb2ec1
--- /dev/null
+++ b/changelogs/unreleased/pravi-gitlab-ce-update-recaptcha.yml
@@ -0,0 +1,5 @@
+---
+title: Apply recaptcha API change in 4.0
+merge_request: 25921
+author: Praveen Arimbrathodiyil
+type: other
diff --git a/changelogs/unreleased/security-shared-project-private-group.yml b/changelogs/unreleased/security-shared-project-private-group.yml
deleted file mode 100644
index 3b21daa5491..00000000000
--- a/changelogs/unreleased/security-shared-project-private-group.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed ability to see private groups by users not belonging to given group
-merge_request:
-author:
-type: security
diff --git a/changelogs/unreleased/sh-cache-root-ref-asymetrically.yml b/changelogs/unreleased/sh-cache-root-ref-asymetrically.yml
new file mode 100644
index 00000000000..106d070cc05
--- /dev/null
+++ b/changelogs/unreleased/sh-cache-root-ref-asymetrically.yml
@@ -0,0 +1,5 @@
+---
+title: Cache Repository#root_ref within a request
+merge_request: 25903
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-blank-codeowners-ce.yml b/changelogs/unreleased/sh-fix-blank-codeowners-ce.yml
new file mode 100644
index 00000000000..05ea5869eb1
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-blank-codeowners-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 500 error caused by CODEOWNERS with no matches
+merge_request: 26072
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-58103.yml b/changelogs/unreleased/sh-fix-issue-58103.yml
deleted file mode 100644
index 1599af23fed..00000000000
--- a/changelogs/unreleased/sh-fix-issue-58103.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Properly handle multiple X-Forwarded-For addresses in runner IP
-merge_request: 25511
-author:
-type: fixed
diff --git a/changelogs/unreleased/sh-revert-rack-request-health-checks.yml b/changelogs/unreleased/sh-revert-rack-request-health-checks.yml
new file mode 100644
index 00000000000..5dd5e5b731c
--- /dev/null
+++ b/changelogs/unreleased/sh-revert-rack-request-health-checks.yml
@@ -0,0 +1,5 @@
+---
+title: Fix health checks not working behind load balancers
+merge_request: 26055
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-rugged-commit-tree-entry.yml b/changelogs/unreleased/sh-rugged-commit-tree-entry.yml
new file mode 100644
index 00000000000..bcefa2c7112
--- /dev/null
+++ b/changelogs/unreleased/sh-rugged-commit-tree-entry.yml
@@ -0,0 +1,5 @@
+---
+title: Bring back Rugged implementation of commit_tree_entry
+merge_request: 25896
+author:
+type: other
diff --git a/changelogs/unreleased/sh-skip-sti-tables-reltuples.yml b/changelogs/unreleased/sh-skip-sti-tables-reltuples.yml
new file mode 100644
index 00000000000..5bf0ccf3e9d
--- /dev/null
+++ b/changelogs/unreleased/sh-skip-sti-tables-reltuples.yml
@@ -0,0 +1,5 @@
+---
+title: Fix counting of groups in admin dashboard
+merge_request: 26009
+author:
+type: fixed
diff --git a/changelogs/unreleased/update-rack-oauth2.yml b/changelogs/unreleased/update-rack-oauth2.yml
new file mode 100644
index 00000000000..dc2e7017695
--- /dev/null
+++ b/changelogs/unreleased/update-rack-oauth2.yml
@@ -0,0 +1,5 @@
+---
+title: Update rack-oauth2 1.2.1 -> 1.9.3
+merge_request: 17868
+author:
+type: other
diff --git a/changelogs/unreleased/use-only-all-pipelines.yml b/changelogs/unreleased/use-only-all-pipelines.yml
new file mode 100644
index 00000000000..68364d2a923
--- /dev/null
+++ b/changelogs/unreleased/use-only-all-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor all_pipelines in Merge request
+merge_request: 25676
+author:
+type: other
diff --git a/config/application.rb b/config/application.rb
index 1c11e347281..6bdf61edfb1 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -94,6 +94,7 @@ module Gitlab
# - Webhook URLs (:hook)
# - Sentry DSN (:sentry_dsn)
# - File content from Web Editor (:content)
+ # - Jira shared secret (:sharedSecret)
#
# NOTE: It is **IMPORTANT** to also update gitlab-workhorse's filter when adding parameters here to not
# introduce another security vulnerability: https://gitlab.com/gitlab-org/gitlab-workhorse/issues/182
@@ -108,6 +109,7 @@ module Gitlab
trace
variables
content
+ sharedSecret
)
# Enable escaping HTML in JSON.
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index 2de310753a9..715e17057e0 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -1,7 +1,28 @@
+# frozen_string_literal: true
+
+return unless Rails.env.test?
+
module RspecProfilingExt
- module PSQL
- def establish_connection
- ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL'])
+ module Collectors
+ class CSVWithTimestamps < ::RspecProfiling::Collectors::CSV
+ TIMESTAMP_FIELDS = %w(created_at updated_at).freeze
+ HEADERS = (::RspecProfiling::Collectors::CSV::HEADERS + TIMESTAMP_FIELDS).freeze
+
+ def insert(attributes)
+ output << HEADERS.map do |field|
+ if TIMESTAMP_FIELDS.include?(field)
+ Time.now
+ else
+ attributes.fetch(field.to_sym)
+ end
+ end
+ end
+
+ private
+
+ def output
+ @output ||= ::CSV.open(path, "w").tap { |csv| csv << HEADERS }
+ end
end
end
@@ -10,9 +31,13 @@ module RspecProfilingExt
if ENV['CI_COMMIT_REF_NAME']
"#{defined?(Gitlab::License) ? 'ee' : 'ce'}:#{ENV['CI_COMMIT_REF_NAME']}"
else
- super
+ super&.chomp
end
end
+
+ def sha
+ super&.chomp
+ end
end
module Run
@@ -30,16 +55,11 @@ module RspecProfilingExt
end
end
-if Rails.env.test?
- RspecProfiling.configure do |config|
- if ENV['RSPEC_PROFILING_POSTGRES_URL'].present?
- RspecProfiling::Collectors::PSQL.prepend(RspecProfilingExt::PSQL)
- config.collector = RspecProfiling::Collectors::PSQL
- end
-
- if ENV.key?('CI')
- RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git)
- RspecProfiling::Run.prepend(RspecProfilingExt::Run)
- end
+RspecProfiling.configure do |config|
+ if ENV.key?('CI') || ENV.key?('RSPEC_PROFILING')
+ RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git)
+ RspecProfiling::Run.prepend(RspecProfilingExt::Run)
+ config.collector = RspecProfilingExt::Collectors::CSVWithTimestamps
+ config.csv_path = -> { "rspec_profiling/#{Time.now.to_i}-#{SecureRandom.hex(8)}-rspec-data.csv" }
end
end
diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb
index 7af465d8443..13896408806 100644
--- a/config/initializers/trusted_proxies.rb
+++ b/config/initializers/trusted_proxies.rb
@@ -14,10 +14,8 @@ module Rack
end
gitlab_trusted_proxies = Array(Gitlab.config.gitlab.trusted_proxies).map do |proxy|
- begin
- IPAddr.new(proxy)
- rescue IPAddr::InvalidAddressError
- end
+ IPAddr.new(proxy)
+rescue IPAddr::InvalidAddressError
end.compact
Rails.application.config.action_dispatch.trusted_proxies = (
diff --git a/config/karma.config.js b/config/karma.config.js
index e1d7c30b1c2..c30c58edc6f 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -6,6 +6,7 @@ const argumentsParser = require('commander');
const webpackConfig = require('./webpack.config.js');
const ROOT_PATH = path.resolve(__dirname, '..');
+const SPECS_PATH = /^(?:\.[\\\/])?(ee[\\\/])?spec[\\\/]javascripts[\\\/]/;
function fatalError(message) {
console.error(chalk.red(`\nError: ${message}\n`));
@@ -26,7 +27,7 @@ webpackConfig.devtool = 'cheap-inline-source-map';
webpackConfig.plugins.push(
new webpack.DefinePlugin({
'process.env.BABEL_ENV': JSON.stringify(process.env.BABEL_ENV || process.env.NODE_ENV || null),
- })
+ }),
);
const specFilters = argumentsParser
@@ -37,13 +38,23 @@ const specFilters = argumentsParser
memo.push(filter, filter.replace(/\/?$/, '/**/*.js'));
return memo;
},
- []
+ [],
)
.parse(process.argv).filterSpec;
-if (specFilters.length) {
- const specsPath = /^(?:\.[\\\/])?spec[\\\/]javascripts[\\\/]/;
+const createContext = (specFiles, regex, suffix) => {
+ const newContext = specFiles.reduce((context, file) => {
+ const relativePath = file.replace(SPECS_PATH, '');
+ context[file] = `./${relativePath}`;
+ return context;
+ }, {});
+
+ webpackConfig.plugins.push(
+ new webpack.ContextReplacementPlugin(regex, path.join(ROOT_PATH, suffix), newContext),
+ );
+};
+if (specFilters.length) {
// resolve filters
let filteredSpecFiles = specFilters.map(filter =>
glob
@@ -51,7 +62,7 @@ if (specFilters.length) {
root: ROOT_PATH,
matchBase: true,
})
- .filter(path => path.endsWith('spec.js'))
+ .filter(path => path.endsWith('spec.js')),
);
// flatten
@@ -64,23 +75,15 @@ if (specFilters.length) {
fatalError('Your filter did not match any test files.');
}
- if (!filteredSpecFiles.every(file => specsPath.test(file))) {
+ if (!filteredSpecFiles.every(file => SPECS_PATH.test(file))) {
fatalError('Test files must be located within /spec/javascripts.');
}
- const newContext = filteredSpecFiles.reduce((context, file) => {
- const relativePath = file.replace(specsPath, '');
- context[file] = `./${relativePath}`;
- return context;
- }, {});
+ const CE_FILES = filteredSpecFiles.filter(file => !file.startsWith('ee'));
+ createContext(CE_FILES, /[^e]{2}[\\\/]spec[\\\/]javascripts$/, 'spec/javascripts');
- webpackConfig.plugins.push(
- new webpack.ContextReplacementPlugin(
- /spec[\\\/]javascripts$/,
- path.join(ROOT_PATH, 'spec/javascripts'),
- newContext
- )
- );
+ const EE_FILES = filteredSpecFiles.filter(file => file.startsWith('ee'));
+ createContext(EE_FILES, /ee[\\\/]spec[\\\/]javascripts$/, 'ee/spec/javascripts');
}
// Karma configuration
@@ -111,18 +114,41 @@ module.exports = function(config) {
],
preprocessors: {
'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
+ 'ee/spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
},
- reporters: ['progress'],
+ reporters: ['mocha'],
webpack: webpackConfig,
webpackMiddleware: { stats: 'errors-only' },
+ plugins: [
+ 'karma-chrome-launcher',
+ 'karma-coverage-istanbul-reporter',
+ 'karma-jasmine',
+ 'karma-junit-reporter',
+ 'karma-mocha-reporter',
+ 'karma-sourcemap-loader',
+ 'karma-webpack',
+ ],
};
if (process.env.CI) {
- karmaConfig.reporters = ['mocha', 'junit'];
+ karmaConfig.reporters.push('junit');
karmaConfig.junitReporter = {
outputFile: 'junit_karma.xml',
useBrowserName: false,
};
+ } else {
+ // ignore 404s in local environment because we are not fixing them and they bloat the log
+ function ignore404() {
+ return (request, response /* next */) => {
+ response.writeHead(404);
+ return response.end('NOT FOUND');
+ };
+ }
+
+ karmaConfig.middleware = ['ignore-404'];
+ karmaConfig.plugins.push({
+ 'middleware:ignore-404': ['factory', ignore404],
+ });
}
if (process.env.BABEL_ENV === 'coverage' || process.env.NODE_ENV === 'coverage') {
diff --git a/config/routes.rb b/config/routes.rb
index 53c6225eff1..bbf00208545 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -43,6 +43,7 @@ Rails.application.routes.draw do
get '/autocomplete/users/:id' => 'autocomplete#user'
get '/autocomplete/projects' => 'autocomplete#projects'
get '/autocomplete/award_emojis' => 'autocomplete#award_emojis'
+ get '/autocomplete/merge_request_target_branches' => 'autocomplete#merge_request_target_branches'
# Search
get 'search' => 'search#show'
diff --git a/config/routes/group.rb b/config/routes/group.rb
index b3015529c6e..b300fcb757f 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -14,6 +14,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
get :issues, as: :issues_group
get :merge_requests, as: :merge_requests_group
get :projects, as: :projects_group
+ get :details, as: :details_group
get :activity, as: :activity_group
put :transfer, as: :transfer_group
# TODO: Remove as part of refactor in https://gitlab.com/gitlab-org/gitlab-ce/issues/49693
@@ -31,6 +32,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
namespace :settings do
resource :ci_cd, only: [:show], controller: 'ci_cd' do
put :reset_registration_token
+ patch :update_auto_devops
end
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 64e6ec49219..20b3f4c0264 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -1,3 +1,4 @@
+const fs = require('fs');
const path = require('path');
const glob = require('glob');
const webpack = require('webpack');
@@ -11,6 +12,10 @@ const ROOT_PATH = path.resolve(__dirname, '..');
const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/cache');
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
+const IS_EE =
+ process.env.EE !== undefined
+ ? JSON.parse(process.env.EE)
+ : fs.existsSync(path.join(ROOT_PATH, 'ee'));
const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
const DEV_SERVER_LIVERELOAD = IS_DEV_SERVER && process.env.DEV_SERVER_LIVERELOAD !== 'false';
@@ -44,6 +49,14 @@ function generateEntries() {
pageEntries.forEach(path => generateAutoEntries(path));
+ if (IS_EE) {
+ const eePageEntries = glob.sync('pages/**/index.js', {
+ cwd: path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
+ });
+ eePageEntries.forEach(path => generateAutoEntries(path, 'ee'));
+ watchAutoEntries.push(path.join(ROOT_PATH, 'ee/app/assets/javascripts/pages/'));
+ }
+
const autoEntryKeys = Object.keys(autoEntriesMap);
autoEntriesCount = autoEntryKeys.length;
@@ -68,6 +81,31 @@ function generateEntries() {
return Object.assign(manualEntries, autoEntries);
}
+const alias = {
+ '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
+ emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
+ empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'),
+ icons: path.join(ROOT_PATH, 'app/views/shared/icons'),
+ images: path.join(ROOT_PATH, 'app/assets/images'),
+ vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
+ vue$: 'vue/dist/vue.esm.js',
+ spec: path.join(ROOT_PATH, 'spec/javascripts'),
+
+ // the following resolves files which are different between CE and EE
+ ee_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'),
+};
+
+if (IS_EE) {
+ Object.assign(alias, {
+ ee: path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
+ ee_empty_states: path.join(ROOT_PATH, 'ee/app/views/shared/empty_states'),
+ ee_icons: path.join(ROOT_PATH, 'ee/app/views/shared/icons'),
+ ee_images: path.join(ROOT_PATH, 'ee/app/assets/images'),
+ ee_spec: path.join(ROOT_PATH, 'ee/spec/javascripts'),
+ ee_else_ce: path.join(ROOT_PATH, 'ee/app/assets/javascripts'),
+ });
+}
+
module.exports = {
mode: IS_PRODUCTION ? 'production' : 'development',
@@ -85,19 +123,7 @@ module.exports = {
resolve: {
extensions: ['.js', '.gql', '.graphql'],
- alias: {
- '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
- emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
- empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'),
- icons: path.join(ROOT_PATH, 'app/views/shared/icons'),
- images: path.join(ROOT_PATH, 'app/assets/images'),
- vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
- vue$: 'vue/dist/vue.esm.js',
- spec: path.join(ROOT_PATH, 'spec/javascripts'),
-
- // the following resolves files which are different between CE and EE
- ee_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'),
- },
+ alias,
},
module: {
@@ -251,7 +277,7 @@ module.exports = {
} else {
resource.request = path.join(
ROOT_PATH,
- 'app/assets/javascripts/vue_shared/components/empty_component.js'
+ 'app/assets/javascripts/vue_shared/components/empty_component.js',
);
}
}),
@@ -267,7 +293,7 @@ module.exports = {
const missingDeps = Array.from(compilation.missingDependencies);
const nodeModulesPath = path.join(ROOT_PATH, 'node_modules');
const hasMissingNodeModules = missingDeps.some(
- file => file.indexOf(nodeModulesPath) !== -1
+ file => file.indexOf(nodeModulesPath) !== -1,
);
// watch for changes to missing node_modules
@@ -278,7 +304,7 @@ module.exports = {
// report our auto-generated bundle count
console.log(
- `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`
+ `${autoEntriesCount} entries from '/pages' automatically added to webpack output.`,
);
callback();
@@ -298,6 +324,10 @@ module.exports = {
reportFilename: path.join(ROOT_PATH, 'webpack-report/index.html'),
statsFilename: path.join(ROOT_PATH, 'webpack-report/stats.json'),
}),
+
+ new webpack.DefinePlugin({
+ 'process.env.EE': JSON.stringify(IS_EE),
+ }),
].filter(Boolean),
devServer: {
diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile
index 0cf478b4f89..96c0d9b7478 100644
--- a/danger/documentation/Dangerfile
+++ b/danger/documentation/Dangerfile
@@ -4,33 +4,19 @@ docs_paths_to_review = helper.changes_by_category[:docs]
unless docs_paths_to_review.empty?
message 'This merge request adds or changes files that require a review ' \
- 'from the Technical Writing team whether before or after merging.'
+ 'from the Technical Writing team.'
markdown(<<~MARKDOWN)
## Documentation review
-The following files will require a review from the [Technical Writing](https://about.gitlab.com/handbook/product/technical-writing/) team:
+The following files require a review from a technical writer:
* #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
-Per the [documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html), the review may occur prior to merging this MR **or** after a maintainer has agreed to review and merge this MR without a tech writer review. (Meanwhile, you are welcome to involve a technical writer at any time if you have questions about writing or updating the documentation. GitLabbers are also welcome to use the [#docs](https://gitlab.slack.com/archives/C16HYA2P5) channel on Slack.)
+The review does not need to block merging this merge request. See the:
-The technical writer who, by default, will review this content or collaborate as needed is dependent on the [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages) to which the content applies:
-
-| Tech writer | Stage(s) |
-| ------------ | ------------------------------------------------------------ |
-| `@marcia` | ~Create ~Release + ~"development guidelines" |
-| `@axil` | ~Distribution ~Gitaly ~Gitter ~Monitor ~Package ~Secure |
-| `@eread` | ~Manage ~Configure ~Geo ~Verify |
-| `@mikelewis` | ~Plan |
-
-**To request a review prior to merging**
-
-- Assign the MR to the applicable technical writer, above. If you are not sure which category the change falls within, or the change is not part of one of these categories, mention one of the usernames above.
-
-**To request a review to commence after merging**
-
-- Create an issue for a doc review [using the Doc Review template](https://gitlab.com/gitlab-org/gitlab-ce/issues/new?issuable_template=Doc%20Review) and assign it to the applicable technicial writer from the table.
+- [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages) for the appropriate technical writer for this review.
+- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
MARKDOWN
unless gitlab.mr_labels.include?('Documentation')
diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile
index 6cf54d0f854..808bc96a0a0 100644
--- a/danger/roulette/Dangerfile
+++ b/danger/roulette/Dangerfile
@@ -58,7 +58,9 @@ changes = helper.changes_by_category
changes.delete(:none)
categories = changes.keys - [:unknown]
-unless changes.empty?
+# Single codebase MRs are reviewed using a slightly different process, so we
+# disable the review roulette for such MRs.
+if changes.any? && !gitlab.mr_labels.include?('single codebase')
team =
begin
helper.project_team
diff --git a/danger/single_codebase/Dangerfile b/danger/single_codebase/Dangerfile
new file mode 100644
index 00000000000..a5938cd6783
--- /dev/null
+++ b/danger/single_codebase/Dangerfile
@@ -0,0 +1,56 @@
+def mention_single_codebase_approvers
+ frontend_maintainers = %w(@filipa @iamphill)
+ backend_maintainers = %w(@rspeicher @rymai @yorickpeterse @godfat)
+
+ rows = []
+ users = []
+
+ if gitlab.mr_labels.include?('frontend')
+ frontend_maintainer = frontend_maintainers.sample
+
+ rows << "| ~frontend | `#{frontend_maintainer}`"
+ users << frontend_maintainer
+ end
+
+ if gitlab.mr_labels.include?('backend')
+ backend_maintainer = backend_maintainers.sample
+
+ rows << "| ~backend | `#{backend_maintainer}`"
+ users << backend_maintainer
+ end
+
+ if rows.empty?
+ backup_maintainer = backend_maintainers.sample
+
+ rows << "| ~frontend / ~backend | `#{backup_maintainer}`"
+ users << backup_maintainer
+ end
+
+ markdown(<<~MARKDOWN.strip)
+ ## Single codebase changes
+
+ This merge request contains changes related to the work of moving towards a
+ [single codebase](https://gitlab.com/groups/gitlab-org/-/epics/802) for
+ Community Edition and Enterprise Edition. These changes will need to be
+ reviewed and approved by the following engineers:
+
+ | Category | Reviewer
+ |----------|---------
+ #{rows.join("\n")}
+
+ To make sure this happens, please follow these steps:
+
+ 1. Add all of the mentioned users to the list of merge request approvals.
+ 2. Assign the merge request to the first person in the above list.
+
+ If you are a reviewer, please follow these steps:
+
+ 1. Review the merge request. If it is good to go, approve it.
+ 2. Once approved, assign to the next person in the above list. If you are
+ the last person in the list, merge the merge request.
+ MARKDOWN
+end
+
+if gitlab.mr_labels.include?('single codebase')
+ mention_single_codebase_approvers
+end
diff --git a/db/fixtures/development/03_settings.rb b/db/fixtures/development/02_settings.rb
index 3a4a5d436bf..3a4a5d436bf 100644
--- a/db/fixtures/development/03_settings.rb
+++ b/db/fixtures/development/02_settings.rb
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/03_project.rb
index 9a5f7cf8175..46018cf68aa 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/03_project.rb
@@ -60,7 +60,7 @@ Sidekiq::Testing.inline! do
path: group_path
)
group.description = FFaker::Lorem.sentence
- group.save
+ group.save!
group.add_owner(User.first)
end
diff --git a/db/fixtures/development/04_labels.rb b/db/fixtures/development/04_labels.rb
new file mode 100644
index 00000000000..b9ae4098d76
--- /dev/null
+++ b/db/fixtures/development/04_labels.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'digest/md5'
+
+class Gitlab::Seeder::GroupLabels
+ def initialize(group, label_per_group: 10)
+ @group = group
+ @label_per_group = label_per_group
+ end
+
+ def seed!
+ @label_per_group.times do
+ label_title = FFaker::Product.brand
+ Labels::CreateService
+ .new(title: label_title, color: "##{Digest::MD5.hexdigest(label_title)[0..5]}")
+ .execute(group: @group)
+ print '.'
+ end
+ end
+end
+
+class Gitlab::Seeder::ProjectLabels
+ def initialize(project, label_per_project: 5)
+ @project = project
+ @label_per_project = label_per_project
+ end
+
+ def seed!
+ @label_per_project.times do
+ label_title = FFaker::Vehicle.model
+ Labels::CreateService
+ .new(title: label_title, color: "##{Digest::MD5.hexdigest(label_title)[0..5]}")
+ .execute(project: @project)
+ print '.'
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ puts "\nGenerating group labels"
+ Group.all.find_each do |group|
+ Gitlab::Seeder::GroupLabels.new(group).seed!
+ end
+
+ puts "\nGenerating project labels"
+ Project.all.find_each do |project|
+ Gitlab::Seeder::ProjectLabels.new(project).seed!
+ end
+end
diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb
index 16243b72f9a..926401d8b9e 100644
--- a/db/fixtures/development/09_issues.rb
+++ b/db/fixtures/development/09_issues.rb
@@ -3,13 +3,17 @@ require './spec/support/sidekiq'
Gitlab::Seeder.quiet do
Project.all.each do |project|
10.times do
+ label_ids = project.labels.pluck(:id).sample(3)
+ label_ids += project.group.labels.sample(3) if project.group
+
issue_params = {
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.sentence,
state: ['opened', 'closed'].sample,
milestone: project.milestones.sample,
assignees: [project.team.users.sample],
- created_at: rand(12).months.ago
+ created_at: rand(12).months.ago,
+ label_ids: label_ids
}
Issues::CreateService.new(project, project.team.users.sample, issue_params).execute
diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index 2051bcff8f0..1952f84ed62 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -12,13 +12,17 @@ Gitlab::Seeder.quiet do
source_branch = branches.pop
target_branch = branches.pop
+ label_ids = project.labels.pluck(:id).sample(3)
+ label_ids += project.group.labels.sample(3) if project.group
+
params = {
source_branch: source_branch,
target_branch: target_branch,
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.sentences(3).join(" "),
milestone: project.milestones.sample,
- assignee: project.team.users.sample
+ assignee: project.team.users.sample,
+ label_ids: label_ids
}
# Only create MRs with users that are allowed to create MRs
diff --git a/db/fixtures/development/22_labeled_issues_seed.rb b/db/fixtures/development/22_labeled_issues_seed.rb
deleted file mode 100644
index 3730e9c7958..00000000000
--- a/db/fixtures/development/22_labeled_issues_seed.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-# Creates a project with labeled issues for an user.
-# Run this single seed file using: rake db:seed_fu FILTER=labeled USER_ID=74.
-# If an USER_ID is not provided it will use the last created user.
-require './spec/support/sidekiq'
-
-class Gitlab::Seeder::LabeledIssues
- include ::Gitlab::Utils
-
- def initialize(user)
- @user = user
- end
-
- def seed!
- Sidekiq::Testing.inline! do
- group = create_group
-
- create_projects(group)
- create_labels(group)
- create_issues(group)
- end
-
- print '.'
- end
-
- private
-
- def create_group
- group_name = "group_of_#{@user.username}_#{SecureRandom.hex(4)}"
-
- group_params = {
- name: group_name,
- path: group_name,
- description: FFaker::Lorem.sentence
- }
-
- Groups::CreateService.new(@user, group_params).execute
- end
-
- def create_projects(group)
- 5.times do
- project_name = "project_#{SecureRandom.hex(6)}"
-
- params = {
- namespace_id: group.id,
- name: project_name,
- description: FFaker::Lorem.sentence,
- visibility_level: Gitlab::VisibilityLevel.values.sample
- }
-
- Projects::CreateService.new(@user, params).execute
- end
- end
-
- def create_labels(group)
- group.projects.each do |project|
- 5.times do
- label_title = FFaker::Vehicle.model
- Labels::CreateService.new(title: label_title, color: "#69D100").execute(project: project)
- end
- end
-
- 10.times do
- label_title = FFaker::Product.brand
- Labels::CreateService.new(title: label_title).execute(group: group)
- end
- end
-
- def create_issues(group)
- # Get only group labels
- group_labels =
- LabelsFinder.new(@user, group_id: group.id).execute.where.not(group_id: nil)
-
- group.projects.each do |project|
- label_ids = project.labels.pluck(:id).sample(5)
- label_ids.push(*group.labels.sample(4))
-
- 20.times do
- issue_params = {
- title: FFaker::Lorem.sentence(6),
- description: FFaker::Lorem.sentence,
- state: 'opened',
- label_ids: label_ids
-
- }
-
- Issues::CreateService.new(project, @user, issue_params).execute if project.project_feature.present?
- end
- end
- end
-end
-
-Gitlab::Seeder.quiet do
- user_id = ENV['USER_ID']
-
- user =
- if user_id.present?
- User.find(user_id)
- else
- User.last
- end
-
- Gitlab::Seeder::LabeledIssues.new(user).seed!
-end
diff --git a/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb b/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb
index d40c61f24b1..b4658bc4017 100644
--- a/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb
+++ b/db/migrate/20170530130129_project_foreign_keys_with_cascading_deletes.rb
@@ -126,11 +126,10 @@ class ProjectForeignKeysWithCascadingDeletes < ActiveRecord::Migration[4.2]
queues.each do |queue|
# Stealing is racy so it's possible a pop might be called on an
# already-empty queue.
- begin
- remove_orphans(*queue.pop(true))
- stolen = true
- rescue ThreadError
- end
+
+ remove_orphans(*queue.pop(true))
+ stolen = true
+ rescue ThreadError
end
break unless stolen
diff --git a/db/migrate/20190225152525_add_auto_dev_ops_enabled_to_namespaces.rb b/db/migrate/20190225152525_add_auto_dev_ops_enabled_to_namespaces.rb
new file mode 100644
index 00000000000..93e7a84fb02
--- /dev/null
+++ b/db/migrate/20190225152525_add_auto_dev_ops_enabled_to_namespaces.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddAutoDevOpsEnabledToNamespaces < ActiveRecord::Migration[5.0]
+ DOWNTIME = false
+
+ def change
+ add_column :namespaces, :auto_devops_enabled, :boolean
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 59a76e21a5f..dda0445e3f2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1377,6 +1377,7 @@ ActiveRecord::Schema.define(version: 20190301182457) do
t.integer "cached_markdown_version"
t.string "runners_token"
t.string "runners_token_encrypted"
+ t.boolean "auto_devops_enabled"
t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree
t.index ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree
t.index ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
diff --git a/doc/administration/build_artifacts.md b/doc/administration/build_artifacts.md
index 623a5321f32..85ae2946bc8 100644
--- a/doc/administration/build_artifacts.md
+++ b/doc/administration/build_artifacts.md
@@ -1 +1,5 @@
+---
+redirect_to: 'job_artifacts.md'
+---
+
This document was moved to [job_artifacts](job_artifacts.md).
diff --git a/doc/administration/git_protocol.md b/doc/administration/git_protocol.md
index 11b2adeeeb8..345e8646f9b 100644
--- a/doc/administration/git_protocol.md
+++ b/doc/administration/git_protocol.md
@@ -5,13 +5,16 @@ description: "Set and configure Git protocol v2"
# Configuring Git Protocol v2
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/46555) in GitLab 11.4.
-> [Temporarily disabled](https://gitlab.com/gitlab-org/gitlab-ce/issues/55769) in GitLab 11.5.8, 11.6.6, 11.7.1, and 11.8+
+> Temporarily disabled (see [confidential issue](../user/project/issues/confidential_issues.md)
+> `https://gitlab.com/gitlab-org/gitlab-ce/issues/55769`) in GitLab 11.5.8, 11.6.6, 11.7.1, and 11.8+.
NOTE: **Note:**
-Git protocol v2 support has been [temporarily disabled](https://gitlab.com/gitlab-org/gitlab-ce/issues/55769),
-as a feature used to hide certain internal references does not function when it
+Git protocol v2 support has been temporarily disabled
+because a feature used to hide certain internal references does not function when it
is enabled, and this has a security impact. Once this problem has been resolved,
-protocol v2 support will be re-enabled.
+protocol v2 support will be re-enabled. For more information, see the
+[confidential issue](../user/project/issues/confidential_issues.md)
+`https://gitlab.com/gitlab-org/gitlab-ce/issues/55769`.
Git protocol v2 improves the v1 wire protocol in several ways and is
enabled by default in GitLab for HTTP requests. In order to enable SSH,
diff --git a/doc/administration/index.md b/doc/administration/index.md
index b723edfc78f..5f368ea8d49 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -41,6 +41,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [System hooks](../system_hooks/system_hooks.md): Notifications when users, projects and keys are changed.
- [Security](../security/README.md): Learn what you can do to further secure your GitLab instance.
- [Usage statistics, version check, and usage ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc.
+- [Global user settings](user_settings.md): Configure instance-wide user permissions.
- [Polling](polling.md): Configure how often the GitLab UI polls for updates.
- [GitLab Pages configuration](pages/index.md): Enable and configure GitLab Pages.
- [GitLab Pages configuration for GitLab source installations](pages/source.md): Enable and configure GitLab Pages on
diff --git a/doc/administration/monitoring/performance/introduction.md b/doc/administration/monitoring/performance/introduction.md
index 37a5388d2fc..7ace0ec5a93 100644
--- a/doc/administration/monitoring/performance/introduction.md
+++ b/doc/administration/monitoring/performance/introduction.md
@@ -1 +1,5 @@
+---
+redirect_to: 'index.md'
+---
+
This document was moved to [another location](index.md).
diff --git a/doc/administration/monitoring/performance/prometheus.md b/doc/administration/monitoring/performance/prometheus.md
index d73ef5d1789..2c5bab46dd9 100644
--- a/doc/administration/monitoring/performance/prometheus.md
+++ b/doc/administration/monitoring/performance/prometheus.md
@@ -1 +1,5 @@
+---
+redirect_to: '../prometheus/index.md'
+---
+
This document was moved to [monitoring/prometheus](../prometheus/index.md).
diff --git a/doc/administration/operations.md b/doc/administration/operations.md
index 4797d2a3206..9cd78105bbb 100644
--- a/doc/administration/operations.md
+++ b/doc/administration/operations.md
@@ -1 +1,5 @@
+---
+redirect_to: 'operations/index.md'
+---
+
This document was moved to [another location](operations/index.md).
diff --git a/doc/administration/operations/speed_up_ssh.md b/doc/administration/operations/speed_up_ssh.md
index 89265b3018b..6dc83c42f53 100644
--- a/doc/administration/operations/speed_up_ssh.md
+++ b/doc/administration/operations/speed_up_ssh.md
@@ -1 +1,5 @@
+---
+redirect_to: 'fast_ssh_key_lookup.md'
+---
+
This document was moved to [another location](fast_ssh_key_lookup.md).
diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md
index 7ad38abe4f5..c39fef907db 100644
--- a/doc/administration/raketasks/storage.md
+++ b/doc/administration/raketasks/storage.md
@@ -34,17 +34,59 @@ export ID_FROM=20
export ID_TO=50
```
-You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen.
-There is a specific Queue you can watch to see how long it will take to finish: **project_migrate_hashed_storage**
+You can monitor the progress in the **Admin Area > Monitoring > Background Jobs** page.
+There is a specific Queue you can watch to see how long it will take to finish:
+`hashed_storage:hashed_storage_project_migrate`
After it reaches zero, you can confirm every project has been migrated by running the commands bellow.
If you find it necessary, you can run this migration script again to schedule missing projects.
-Any error or warning will be logged in the sidekiq's log file.
+Any error or warning will be logged in Sidekiq's log file.
You only need the `gitlab:storage:migrate_to_hashed` rake task to migrate your repositories, but we have additional
commands below that helps you inspect projects and attachments in both legacy and hashed storage.
+## Rollback from Hashed storage to Legacy storage
+
+If you need to rollback the storage migration for any reason, you can follow the steps described here.
+
+NOTE: **Note:** Hashed Storage will be required in future version of GitLab.
+
+To prevent new projects from being created in the Hashed storage,
+you need to undo the [enable hashed storage][storage-migration] changes.
+
+This task will schedule all your existing projects and associated attachments to be rolled back to the
+Legacy storage type.
+
+For Omnibus installations, run the following:
+
+```bash
+sudo gitlab-rake gitlab:storage:rollback_to_legacy
+```
+
+For source installations, run the following:
+
+```bash
+sudo -u git -H bundle exec rake gitlab:storage:rollback_to_legacy RAILS_ENV=production
+```
+
+Both commands accept a range as environment variable:
+
+```bash
+# to rollback any migrated project from ID 20 to 50.
+export ID_FROM=20
+export ID_TO=50
+```
+
+You can monitor the progress in the **Admin Area > Monitoring > Background Jobs** page.
+On the **Queues** tab, you can watch the `hashed_storage:hashed_storage_project_rollback` queue to see how long the process will take to finish.
+
+
+After it reaches zero, you can confirm every project has been rolled back by running the commands bellow.
+If some projects weren't rolled back, you can run this rollback script again to schedule further rollbacks.
+
+Any error or warning will be logged in Sidekiq's log file.
+
## List projects on Legacy storage
To have a simple summary of projects using **Legacy** storage:
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 4934aaf39f7..40f7c5566ac 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -2,6 +2,24 @@
> [Introduced][ce-28283] in GitLab 10.0.
+Two different storage layouts can be used
+to store the repositories on disk and their characteristics.
+
+GitLab can be configured to use one or multiple repository shard locations
+that can be:
+
+- Mounted to the local disk
+- Exposed as an NFS shared volume
+- Acessed via [gitaly] on its own machine.
+
+In GitLab, this is configured in `/etc/gitlab/gitlab.rb` by the `git_data_dirs({})`
+configuration hash. The storage layouts discussed here will apply to any shard
+defined in it.
+
+The `default` repository shard that is available in any installations
+that haven't customized it, points to the local folder: `/var/opt/gitlab/git-data`.
+Anything discussed below is expected to be part of that folder.
+
## Legacy Storage
Legacy Storage is the storage behavior prior to version 10.0. For historical
@@ -66,34 +84,7 @@ by another folder with the next 2 characters. They are both stored in a special
"@hashed/#{hash[0..1]}/#{hash[2..3]}/#{hash}.wiki.git"
```
-### How to migrate to Hashed Storage
-
-In GitLab, go to **Admin > Settings**, find the **Repository Storage** section
-and select "_Use hashed storage paths for newly created and renamed projects_".
-
-To migrate your existing projects to the new storage type, check the specific
-[rake tasks].
-
-[ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283
-[rake tasks]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage
-[storage-paths]: repository_storage_types.md
-
-#### Rollback
-
-There is no automated rollback implemented. Below are the steps required to rollback
-from each storage migration.
-
-The rollback has to be performed in the reverse order. To get into "Legacy" state,
-you need to rollback Attachments first, then Project.
-
-Also note that if Geo is enabled, after the migration was triggered, an event is generated
-to replicate the operation on any Secondary node. That means the on disk changes will also
-need to be performed on these nodes as well. Database changes will propagate without issues.
-
-You must make sure the migration event was already processed or otherwise it may migrate
-the files back to Hashed state again.
-
-#### Hashed object pools
+### Hashed object pools
For deduplication of public forks and their parent repository, objects are pooled
in an object pool. These object pools are a third repository where shared objects
@@ -110,36 +101,60 @@ enabled for individual projects by executing
be on hashed storage, should not be a fork itself, and hashed storage should be
enabled for all new projects.
-##### Attachments
+### How to migrate to Hashed Storage
-To rollback single Attachment migration, rename `aa/bb/abcdef1234567890...` folder back to `namespace/project`.
+To start a migration, enable Hashed Storage for new projects:
+
+1. Go to **Admin > Settings** and expand the **Repository Storage** section.
+2. Select the **Use hashed storage paths for newly created and renamed projects** checkbox.
-Both folder names can be generated by the `FileUploader.absolute_base_dir(project)`, you
-just need to switch the version from the `project` back to the previous one.
+Check if the change breaks any existing integration you may have that
+either runs on the same machine as your repositories are located, or may login to that machine
+to access data (for example, a remote backup solution).
-```ruby
-project.storage_version
-# => 2
+To schedule a complete rollout, see the
+[rake task documentation for storage migration][rake/migrate-to-hashed] for instructions.
-FileUploader.absolute_base_dir(project)
-# => "/opt/gitlab/embedded/service/gitlab-rails/public/uploads/@hashed/d4/73/d4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35"
+If you do have any existing integration, you may want to do a small rollout first,
+to validate. You can do so by specifying a range with the operation.
-project.storage_version = 1
+This is an example of how to limit the rollout to Project IDs 50 to 100, running in
+an Omnibus Gitlab installation:
-FileUploader.absolute_base_dir(project)
-# => "/opt/gitlab/embedded/service/gitlab-rails/public/uploads/gitlab/gitlab-shell-renamed"
+```bash
+sudo gitlab-rake gitlab:storage:migrate_to_hashed ID_FROM=50 ID_TO=100
```
-##### Project
+Check the [documentation][rake/migrate-to-hashed] for additional information and instructions for
+source-based installation.
+
+#### Rollback
+
+Similar to the migration, to disable Hashed Storage for new
+projects:
-To rollback single Project migration, move `@hashed/aa/bb/aabbcdef1234567890abcdef.git` and `@hashed/aa/bb/aabbcdef1234567890abcdef.wiki.git`
-back to `namespace/project.git` and `namespace/project.wiki.git` respectively and switch the version from the `project` back to `null`.
+1. Go to **Admin > Settings** and expand the **Repository Storage** section.
+2. Uncheck the **Use hashed storage paths for newly created and renamed projects** checkbox.
+
+To schedule a complete rollback, see the
+[rake task documentation for storage rollback][rake/rollback-to-legacy] for instructions.
+
+The rollback task also supports specifying a range of Project IDs. Here is an example
+of limiting the rollout to Project IDs 50 to 100, in an Omnibus Gitlab installation:
+
+```bash
+sudo gitlab-rake gitlab:storage:rollback_to_legacy ID_FROM=50 ID_TO=100
+```
+
+If you have a Geo setup, please note that the rollback will not be reflected automatically
+on the **secondary** node. You may need to wait for a backfill operation to kick-in and remove
+the remaining repositories from the special `@hashed/` folder manually.
### Hashed Storage coverage
We are incrementally moving every storable object in GitLab to the Hashed
Storage pattern. You can check the current coverage status below (and also see
-the [issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821)).
+the [issue][ce-2821]).
Note that things stored in an S3 compatible endpoint will not have the downsides
mentioned earlier, if they are not prefixed with `#{namespace}/#{project_name}`,
@@ -156,6 +171,7 @@ which is true for CI Cache and LFS Objects.
| CI Artifacts | No | No | Yes | 9.4 / 10.6 |
| CI Cache | No | No | Yes | - |
| LFS Objects | Yes | Similar | Yes | 10.0 / 10.7 |
+| Repository pools| No | Yes | - | 11.6 |
#### Implementation Details
@@ -180,3 +196,10 @@ LFS Objects implements a similar storage pattern using 2 chars, 2 level folders,
```
They are also S3 compatible since **10.0** (GitLab Premium), and available in GitLab Core since **10.7**.
+
+[ce-2821]: https://gitlab.com/gitlab-com/infrastructure/issues/2821
+[ce-28283]: https://gitlab.com/gitlab-org/gitlab-ce/issues/28283
+[rake/migrate-to-hashed]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage
+[rake/rollback-to-legacy]: raketasks/storage.md#rollback
+[storage-paths]: repository_storage_types.md
+[gitaly]: gitaly/index.md
diff --git a/doc/administration/repository_storages.md b/doc/administration/repository_storages.md
index cf6de15743f..af7a385e5a0 100644
--- a/doc/administration/repository_storages.md
+++ b/doc/administration/repository_storages.md
@@ -1 +1,5 @@
+---
+redirect_to: 'repository_storage_paths.md'
+---
+
This document was moved to [another location](repository_storage_paths.md).
diff --git a/doc/administration/user_settings.md b/doc/administration/user_settings.md
new file mode 100644
index 00000000000..f9654655949
--- /dev/null
+++ b/doc/administration/user_settings.md
@@ -0,0 +1,35 @@
+# Modifying global user settings
+
+GitLab administrators can modify user settings for the entire GitLab instance.
+
+## Disallow users creating top-level groups
+
+By default, new users can create top-level groups. To disable this, modify the appropriate configuration file.
+
+For Omnibus installations, add the following to `/etc/gitlab/gitlab.rb`:
+
+```ruby
+gitlab_rails['gitlab_default_can_create_group'] = false
+```
+
+For source installations, uncomment the following line in `config/gitlab.yml`:
+
+```yaml
+# default_can_create_group: false # default: true
+```
+
+## Disallow users changing usernames
+
+By default, new users can change their usernames. To disable this, modify the appropriate configuration file.
+
+For Omnibus installations, add the following to `/etc/gitlab/gitlab.rb`:
+
+```ruby
+gitlab_rails['gitlab_username_changing_enabled'] = false
+```
+
+For source installations, uncomment the following line in `config/gitlab.yml`:
+
+```yaml
+# username_changing_enabled: false # default: true - User can change her username/namespace
+```
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index b8bc4c40124..b66a3198ffb 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -64,9 +64,9 @@ Get all namespaces that match a string in their name or path.
GET /namespaces?search=foobar
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `search` | string | no | Returns a list of namespaces the user is authorized to see based on the search criteria |
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
+| `search` | string | no | Returns a list of namespaces the user is authorized to see based on the search criteria |
Example request:
@@ -98,9 +98,9 @@ Get a namespace by ID.
GET /namespaces/:id
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | ID or path of the namespace |
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | ID or [URL-encoded path of the namespace](README.md#namespaced-path-encoding) |
Example request:
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
index 137f1fdddec..2e5b8df9a83 100644
--- a/doc/api/pipeline_schedules.md
+++ b/doc/api/pipeline_schedules.md
@@ -1,4 +1,4 @@
-# Pipeline schedules
+# Pipeline schedules API
You can read more about [pipeline schedules](../user/project/pipelines/schedules.md).
@@ -280,7 +280,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gi
## Pipeline schedule variable
-> [Introduced][ce-34518] in GitLab 10.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/34518) in GitLab 10.0.
## Create a new pipeline schedule variable
@@ -358,5 +358,3 @@ curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gi
"value": "updated value"
}
```
-
-[ce-34518]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34518 \ No newline at end of file
diff --git a/doc/api/search.md b/doc/api/search.md
index aa601648b2c..330047e323b 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -253,7 +253,7 @@ Example response:
### Scope: snippet_blobs
```bash
-curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=snippet_blos&search=test
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=snippet_blobs&search=test
```
Example response:
diff --git a/doc/ci/README.md b/doc/ci/README.md
index c66a1d4b7a8..47810a8b7b6 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -43,24 +43,28 @@ For complete control, you can manually configure GitLab CI/CD.
With basic knowledge of how GitLab CI/CD works, the following documentation extends your knowledge
into more features:
-| Topic | Description |
-|:-------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------|
-| [Introduction to pipelines and jobs](pipelines.md) | Provides an overview of GitLab CI/CD and jobs. |
-| [CI/CD Variables](variables/README.md) | How environment variables can be configured and made available in pipelines. |
-| [Where variables can be used](variables/where_variables_can_be_used.md) | A deeper look into where and how CI/CD variables can be used. |
-| [User](../user/permissions.md#gitlab-ci) and [job](../user/permissions.md#job-permissions) permissions | Learn about the access levels a user can have for performing certain CI actions. |
-| [Configuring GitLab Runners](runners/README.md) | Documentation for configuring [GitLab Runner](https://docs.gitlab.com/runner/). |
-| [Introduction to environments and deployments](environments.md) | Learn how to separate your jobs into environments and use them for different purposes like testing, building and, deploying. |
-| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Learn about the output of jobs. |
-| [Cache dependencies in GitLab CI/CD](caching/index.md) | Discover how to speed up pipelines using caching. |
-| [Using Git submodules with GitLab CI](git_submodules.md) | How to run your CI jobs when using Git submodules. |
-| [Pipelines for merge requests](merge_request_pipelines/index.md) | Create pipelines specifically for merge requests. |
-| [Using SSH keys with GitLab CI/CD](ssh_keys/README.md) | Use SSH keys in your build environment. |
-| [Triggering pipelines through the API](triggers/README.md) | Use the GitLab API to trigger a pipeline. |
-| [Pipeline schedules](../user/project/pipelines/schedules.md) | Trigger pipelines on a schedule. |
-| [Connecting GitLab with a Kubernetes cluster](../user/project/clusters/index.md) | Integrate one or more Kubernetes clusters to your project. |
-| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. |
-| [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. |
+| Topic | Description |
+|:--------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------|
+| [Creating and using CI/CD pipelines](pipelines.md) | Understand, visualize, create, and use CI/CD pipelines. |
+| [CI/CD Variables](variables/README.md) | How environment variables can be configured and made available in pipelines. |
+| [Where variables can be used](variables/where_variables_can_be_used.md) | A deeper look into where and how CI/CD variables can be used. |
+| [User](../user/permissions.md#gitlab-cicd-permissions) and [job](../user/permissions.md#job-permissions) permissions | Learn about the access levels a user can have for performing certain CI actions. |
+| [Configuring GitLab Runners](runners/README.md) | Documentation for configuring [GitLab Runner](https://docs.gitlab.com/runner/). |
+| [Introduction to environments and deployments](environments.md) | Learn how to separate your jobs into environments and use them for different purposes like testing, building and, deploying. |
+| [Job artifacts](../user/project/pipelines/job_artifacts.md) | Learn about the output of jobs. |
+| [Cache dependencies in GitLab CI/CD](caching/index.md) | Discover how to speed up pipelines using caching. |
+| [Using Git submodules with GitLab CI](git_submodules.md) | How to run your CI jobs when using Git submodules. |
+| [Pipelines for merge requests](merge_request_pipelines/index.md) | Create pipelines specifically for merge requests. |
+| [Using SSH keys with GitLab CI/CD](ssh_keys/README.md) | Use SSH keys in your build environment. |
+| [Triggering pipelines through the API](triggers/README.md) | Use the GitLab API to trigger a pipeline. |
+| [Pipeline schedules](../user/project/pipelines/schedules.md) | Trigger pipelines on a schedule. |
+| [Connecting GitLab with a Kubernetes cluster](../user/project/clusters/index.md) | Integrate one or more Kubernetes clusters to your project. |
+| [ChatOps](chatops/README.md) | Trigger CI jobs from chat, with results sent back to the channel. |
+| [Interactive web terminals](interactive_web_terminal/index.md) | Open an interactive web terminal to debug the running jobs. |
+| [Review Apps](review_apps/index.md) | Configure GitLab CI/CD to preview code changes in a per-branch basis. |
+| [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html) **[PREMIUM]** | Check the current health and status of each CI/CD environment running on Kubernetes. |
+| [GitLab CI/CD for external repositories](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/index.html) **[PREMIUM]** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and BitBucket Cloud. |
+| [Protected environments](https://docs.gitlab.com/ce/ci/environments/protected_environments.html) **[PREMIUM]** | Ensure that only people with the right privileges can deploy to an environment. |
### GitLab Pages
diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md
index 985ec4b972c..5221cbf8609 100644
--- a/doc/ci/autodeploy/index.md
+++ b/doc/ci/autodeploy/index.md
@@ -1 +1,5 @@
+---
+redirect_to: '../../topics/autodevops/index.md#auto-deploy'
+---
+
This document was moved to [another location](../../topics/autodevops/index.md#auto-deploy).
diff --git a/doc/ci/autodeploy/quick_start_guide.md b/doc/ci/autodeploy/quick_start_guide.md
index 985ec4b972c..5221cbf8609 100644
--- a/doc/ci/autodeploy/quick_start_guide.md
+++ b/doc/ci/autodeploy/quick_start_guide.md
@@ -1 +1,5 @@
+---
+redirect_to: '../../topics/autodevops/index.md#auto-deploy'
+---
+
This document was moved to [another location](../../topics/autodevops/index.md#auto-deploy).
diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md
index 22b3872025f..b63659c1878 100644
--- a/doc/ci/build_artifacts/README.md
+++ b/doc/ci/build_artifacts/README.md
@@ -1 +1,5 @@
+---
+redirect_to: '../../user/project/pipelines/job_artifacts.md'
+---
+
This document was moved to [pipelines/job_artifacts.md](../../user/project/pipelines/job_artifacts.md).
diff --git a/doc/ci/chatops/README.md b/doc/ci/chatops/README.md
index 6ad1df7bb2a..df7fb8a4912 100644
--- a/doc/ci/chatops/README.md
+++ b/doc/ci/chatops/README.md
@@ -12,7 +12,7 @@ GitLab ChatOps provides a method to interact with CI/CD jobs through chat servic
GitLab ChatOps is built upon two existing features, [GitLab CI/CD](../README.md) and [Slack Slash Commmands](../../user/project/integrations/slack_slash_commands.md).
-A new `run` action has been added to the [slash commands](../../integration/slash_commands.md), which takes two arguments: a `<job name>` to execute and the `<job arguments>`. When executed, ChatOps will look up the specified job name and attempt to match it to a corresponding job in [.gitlab-ci.yml](../yaml/README.md). If a matching job is found on `master`, a pipeline containing just that job is scheduled. Two additional [CI/CD variables](../variables/README.html#predefined-variables-environment-variables) are passed to the job: `CHAT_INPUT` contains any additional arguments, and `CHAT_CHANNEL` is set to the name of channel the action was triggered in.
+A new `run` action has been added to the [slash commands](../../integration/slash_commands.md), which takes two arguments: a `<job name>` to execute and the `<job arguments>`. When executed, ChatOps will look up the specified job name and attempt to match it to a corresponding job in [.gitlab-ci.yml](../yaml/README.md). If a matching job is found on `master`, a pipeline containing just that job is scheduled. Two additional [CI/CD variables](../variables/README.md#predefined-environment-variables) are passed to the job: `CHAT_INPUT` contains any additional arguments, and `CHAT_CHANNEL` is set to the name of channel the action was triggered in.
After the job has finished, its output is sent back to Slack provided it has completed within 30 minutes. If a job takes more than 30 minutes to run it must use the Slack API to manually send data back to a channel.
diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
index 04633fa9dc4..90a8f5917f8 100644
--- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
@@ -4,6 +4,7 @@ author_gitlab: blitzgren
level: intermediate
article_type: tutorial
date: 2018-03-07
+last_updated: 2019-03-06
---
# DevOps and Game Dev with GitLab CI/CD
@@ -520,7 +521,7 @@ a lot of breathing room in quickly getting changes to players.
Here are some ideas to further investigate that can speed up or improve your pipeline:
- [Yarn](https://yarnpkg.com) instead of npm
-- Set up a custom [Docker](../../../ci/docker/using_docker_images.md#define-image-and-services-from-gitlab-ci-yml) image that can preload dependencies and tools (like AWS CLI)
+- Set up a custom [Docker](../../../ci/docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) image that can preload dependencies and tools (like AWS CLI)
- Forward a [custom domain](http://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) to your game's S3 static website
- Combine jobs if you find it unnecessary for a small project
- Avoid the queues and set up your own [custom GitLab CI/CD runner](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
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 3963a3e511d..0f809ed05ca 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -5,6 +5,7 @@ author_gitlab: mehranrasulian
level: intermediate
article_type: tutorial
date: 2017-08-31
+last_updated: 2019-03-06
---
# Test and deploy Laravel applications with GitLab CI/CD and Envoy
@@ -268,7 +269,7 @@ 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 }}
+ cd {{ $new_release_dir }}
git reset --hard {{ $commit }}
@endtask
@@ -346,7 +347,7 @@ 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 }}
+ cd {{ $new_release_dir }}
git reset --hard {{ $commit }}
@endtask
@@ -374,7 +375,7 @@ You might want to create another Envoy task to do that for you.
We also create the `.env` file in the same path to set up our production environment variables for Laravel.
These are persistent data and will be shared to every new release.
-Now, we would need to deploy our app by running `envoy run deploy`, but it won't be necessary since GitLab can handle that for us with CI's [environments](../../environments.md), which will be described [later](#setting-up-gitlab-ci-cd) in this tutorial.
+Now, we would need to deploy our app by running `envoy run deploy`, but it won't be necessary since GitLab can handle that for us with CI's [environments](../../environments.md), which will be described [later](#setting-up-gitlab-cicd) in this tutorial.
Now it's time to commit [Envoy.blade.php](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Envoy.blade.php) and push it to the `master` branch.
To keep things simple, we commit directly to `master`, without using [feature-branches](../../../workflow/gitlab_flow.md#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial.
@@ -557,7 +558,7 @@ So we should adjust the configuration of MySQL instance by defining `MYSQL_DATAB
Find out more about MySQL variables at the [official MySQL Docker Image](https://hub.docker.com/r/_/mysql/).
Also set the variables `DB_HOST` to `mysql` and `DB_USERNAME` to `root`, which are Laravel specific variables.
-We define `DB_HOST` as `mysql` instead of `127.0.0.1`, as we use MySQL Docker image as a service which [is linked to the main Docker image](../../docker/using_docker_images.md#how-services-are-linked-to-the-build).
+We define `DB_HOST` as `mysql` instead of `127.0.0.1`, as we use MySQL Docker image as a service which [is linked to the main Docker image](../../docker/using_docker_images.md#how-services-are-linked-to-the-job).
```yaml
...
diff --git a/doc/ci/examples/sast_docker.md b/doc/ci/examples/sast_docker.md
index 3a657b3a3d5..70b269046e5 100644
--- a/doc/ci/examples/sast_docker.md
+++ b/doc/ci/examples/sast_docker.md
@@ -1 +1,5 @@
-This document was moved to [another location](./container_scanning.md).
+---
+redirect_to: 'container_scanning.md'
+---
+
+This document was moved to [another location](container_scanning.md).
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
index 24328bf6c02..fa18cb22aed 100644
--- a/doc/ci/examples/test-scala-application.md
+++ b/doc/ci/examples/test-scala-application.md
@@ -55,8 +55,8 @@ You can use other versions of Scala and SBT by defining them in
Add the `Coverage was \[\d+.\d+\%\]` regular expression in the
**Settings ➔ Pipelines ➔ Coverage report** project setting to
-retrieve the [test coverage] rate from the build trace and have it
-displayed with your jobs.
+retrieve the [test coverage](../../user/project/pipelines/settings.md#test-coverage-report-badge)
+rate from the build trace and have it displayed with your jobs.
**Pipelines** must be enabled for this option to appear.
@@ -69,8 +69,5 @@ in the `.gitlab-ci.yml` file with your application's name.
## Heroku API key
You can look up your Heroku API key in your
-[account](https://dashboard.heroku.com/account). Add a secure [variable] with
+[account](https://dashboard.heroku.com/account). Add a [protected variable](../variables/README.md#protected-variables) with
this value in **Project ➔ Variables** with key `HEROKU_API_KEY`.
-
-[variable]: ../variables/README.md#user-defined-variables-secure-variables
-[test coverage]: ../../user/project/pipelines/settings.md#test-coverage-report-badge
diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
index f24e79355aa..1a909e8892a 100644
--- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
@@ -4,6 +4,7 @@ author_gitlab: Hostert
level: beginner
article_type: tutorial
date: 2018-02-20
+last_updated: 2019-03-06
---
# Testing a Phoenix application with GitLab CI/CD
@@ -177,7 +178,7 @@ environment it can run. Since we will work with a single environment, we'll edit
configuration file (`test.exs`).
But, why do we need to adjust our configuration? Well, GitLab CI/CD builds and tests our code in one
-isolated virtual machine, called [Runner][runner-site], using Docker technology. In this Runner,
+isolated virtual machine, called [Runner](../../runners/README.md), using Docker technology. In this Runner,
GitLab CI/CD has access to everything our Phoenix application need to run, exactly as we have in our
`localhost`, but we have to tell GitLab CI/CD where to create and find this database using system
variables. This way, GitLab CI/CD will create our test database inside the Runner, just like we do
@@ -417,7 +418,6 @@ other reasons][ci-reasons] to keep using GitLab CI/CD. The benefits to our teams
[ci-docs]: ../../README.md "GitLab CI/CD Documentation"
[skipping-jobs]: ../../yaml/README.md#skipping-jobs "Skipping Jobs"
[gitlab-runners]: ../../runners/README.md "GitLab Runners Documentation"
-[runner-site]: ../../runners/README.md#runners "Runners"
[docker-image]: https://hub.docker.com/r/trenpixster/elixir/ "Elixir Docker Image"
[using-docker]: ../../docker/using_docker_images.md "Using Docker Images"
[hello-gitlab]: https://gitlab.com/Hostert/hello_gitlab_ci "Hello GitLab CI/CD"
diff --git a/doc/ci/img/pipelines-goal.png b/doc/ci/img/pipelines-goal.png
deleted file mode 100644
index f15716d0b8f..00000000000
--- a/doc/ci/img/pipelines-goal.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/types-of-pipelines.png b/doc/ci/img/types-of-pipelines.png
deleted file mode 100644
index 829a53d5d52..00000000000
--- a/doc/ci/img/types-of-pipelines.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index 2af0a03cbe4..edb5f8c9ff8 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -9,13 +9,18 @@ For example, unit tests, lint checks, and [Review Apps](../review_apps/index.md)
are often used in this cycle.
With pipelines for merge requests, you can design a specific pipeline structure
-for merge requests. All you need to do is just adding `only: [merge_requests]` to
-the jobs that you want it to run for only merge requests.
-Every time, when developers create or update merge requests, a pipeline runs on
+for merge requests.
+
+## Configuring pipelines for merge requests
+
+To configure pipelines for merge request, add the `only: merge_requests` parameter to
+the jobs that you want it to run only for merge requests.
+
+Then, when developers create or update merge requests, a pipeline runs on
their new commits at every push to GitLab.
NOTE: **Note**:
-If you use both this feature and [Merge When Pipeline Succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md),
+If you use this feature with [merge when pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md),
pipelines for merge requests take precedence over the other regular pipelines.
For example, consider the following [`.gitlab-ci.yml`](../yaml/README.md):
@@ -40,15 +45,17 @@ deploy:
script: ./deploy
```
-After the merge request is updated with new commits, GitLab detects that changes
-have occurred and creates a new pipeline for the merge request.
-The pipeline fetches the latest code from the source branch and run tests against it.
+After the merge request is updated with new commits:
+
+- GitLab detects that changes have occurred and creates a new pipeline for the merge request.
+- The pipeline fetches the latest code from the source branch and run tests against it.
+
In the above example, the pipeline contains only `build` and `test` jobs.
-Since the `deploy` job doesn't have the `only: [merge_requests]` rule,
+Since the `deploy` job doesn't have the `only: merge_requests` rule,
deployment jobs will not happen in the merge request.
-Pipelines tagged as **merge request** indicate that they were triggered
-when a merge request was created or updated.
+Pipelines tagged with **merge request** badge indicate that they were triggered
+when a merge request was created or updated. For example:
![Merge request page](img/merge_request.png)
@@ -62,9 +69,14 @@ The behavior of the `only: merge_requests` rule is such that _only_ jobs with
that rule are run in the context of a merge request; no other jobs will be run.
However, you may want to reverse this behaviour, having all of your jobs to run _except_
-for one or two. Consider the following pipeline, with jobs `A`, `B`, and `C`. If you want
-all pipelines to always run `A` and `B`, but only want `C` to run for a merge request,
-you can configure your `.gitlab-ci.yml` file as follows:
+for one or two.
+
+Consider the following pipeline, with jobs `A`, `B`, and `C`. Imagine you want:
+
+- All pipelines to always run `A` and `B`
+- Only want `C` to run for a merge request,
+
+To achieve this, you can configure your `.gitlab-ci.yml` file as follows:
``` yaml
.only-default: &only-default
@@ -90,9 +102,11 @@ C:
- merge_requests
```
-Since `A` and `B` are getting the `only:` rule to execute in all cases, they will
-always run. `C` specifies that it should only run for merge requests, so for any
-pipeline except a merge request pipeline, it will not run.
+Because:
+
+- `A` and `B` are getting the `only:` rule to execute in all cases, they will always run.
+- `C` specifies that it should only run for merge requests, it will not run for any pipeline
+ except a merge request pipeline.
As you can see, this will help you avoid a lot of boilerplate where you'd need
to add that `only:` rule to all of your jobs in order to make them always run. You
diff --git a/doc/ci/permissions/README.md b/doc/ci/permissions/README.md
index 80d8e46f29c..bc1e6ce3e0b 100644
--- a/doc/ci/permissions/README.md
+++ b/doc/ci/permissions/README.md
@@ -1 +1,5 @@
-This document was moved to [user/permissions.md](../../user/permissions.md#gitlab-ci).
+---
+redirect_to: '../../user/permissions.md#gitlab-cicd-permissions'
+---
+
+This document was moved to [user/permissions.md](../../user/permissions.md#gitlab-cicd-permissions).
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 4f3106c6dc6..a77389332c4 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -1,271 +1,331 @@
-# Introduction to pipelines and jobs
+# Creating and using CI/CD pipelines
> Introduced in GitLab 8.8.
+## Introduction
+
+Pipelines are the top-level component of continuous integration, deployment, and delivery.
+
+Pipelines comprise:
+
+- Jobs that define what to run. For example, code compilation or test runs.
+- Stages that define when and how to run. For example, that test run after code compilation.
+
+Jobs in a stage are executed by [Runners](runners/README.md) in parallel, if there are enough concurrent [Runners](runners/README.md).
+
+If the jobs in a stage:
+
+- Succeed, the pipeline moves on to the next stage.
+- Fail, the next stage is not (usually) executed.
+
NOTE: **Note:**
If you have a [mirrored repository where GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository-starter),
you may need to enable pipeline triggering in your project's
**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
-## Pipelines
+### Simple example
-A pipeline is a group of [jobs] that get executed in [stages].
-All of the jobs in a stage are executed in parallel (if there are enough
-concurrent [Runners]), and if they all succeed, the pipeline moves on to the
-next stage. If one of the jobs fails, the next stage is not (usually)
-executed. You can access the pipelines page in your project's **Pipelines** tab.
+As an example, imagine a pipeline consisting of four stages, executed in the following order:
-In the following image you can see that the pipeline consists of four stages
-(`build`, `test`, `staging`, `production`) each one having one or more jobs.
+- `build`, with a job called `compile`.
+- `test`, with two jobs called `test` and `test2`.
+- `staging`, with a job called `deploy-to-stage`.
+- `production`, with a job called `deploy-to-prod`.
->**Note:**
-GitLab capitalizes the stages' names when shown in the [pipeline graphs](#pipeline-graphs).
+## Visualizing pipelines
-![Pipelines example](img/pipelines.png)
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5742) in GitLab 8.11.
-## Types of pipelines
-
-There are three types of pipelines that often use the single shorthand of "pipeline". People often talk about them as if each one is "the" pipeline, but really, they're just pieces of a single, comprehensive pipeline.
+Pipelines can be complex structures with many sequential and parallel jobs.
-![Types of Pipelines](img/types-of-pipelines.png)
+To make it easier to understand the flow of a pipeline, GitLab has pipeline graphs for viewing pipeline
+and their statuses.
-1. **CI Pipeline**: Build and test stages defined in `.gitlab-ci.yml`.
-1. **Deploy Pipeline**: Deploy stage(s) defined in `.gitlab-ci.yml` The flow of deploying code to servers through various stages: e.g. development to staging to production.
-1. **Project Pipeline**: Cross-project CI dependencies [triggered via API][triggers], particularly for micro-services, but also for complicated build dependencies: e.g. api -> front-end, ce/ee -> omnibus.
+Pipeline graphs can be displayed in two different ways, depending on what page you
+access the graph.
-## Development workflows
+NOTE: **Note:**
+GitLab capitalizes the stages' names when shown in the [pipeline graphs](#pipeline-graphs).
-Pipelines accommodate several development workflows:
+### Regular pipeline graphs
-1. **Branch Flow** (e.g. different branch for dev, qa, staging, production).
-1. **Trunk-based Flow** (e.g. feature branches and single master branch, possibly with tags for releases).
-1. **Fork-based Flow** (e.g. merge requests come from forks).
+Regular pipeline graphs that show the names of the jobs of each stage. Regular pipeline graphs can
+be found when you are on a [single pipeline page](#seeing-pipeline-status). For example:
-Example continuous delivery flow:
+![Pipelines example](img/pipelines.png)
-![CD Flow](img/pipelines-goal.png)
+### Pipeline mini graphs
-## Jobs
+Pipeline mini graphs takes less space and can give you a
+quick glance if all jobs pass or something failed. The pipeline mini graph can
+be found when you navigate to:
-Jobs can be defined in the [`.gitlab-ci.yml`][jobs-yaml] file. Not to be
-confused with a `build` job or `build` stage.
+- The pipelines index page.
+- A single commit page.
+- A merge request page.
-## Defining pipelines
+Pipeline mini graphs allow you to see all related jobs for a single commit and the net result
+of each stage of your pipeline. This allows you to quickly see what failed and
+fix it.
-Pipelines are defined in `.gitlab-ci.yml` by specifying [jobs] that run in
-[stages].
+Stages in pipeline mini graphs are collapsible. Hover your mouse over them and click to expand their jobs.
-See the reference [documentation for jobs](yaml/README.md#jobs).
+| Mini graph | Mini graph expanded |
+|:-------------------------------------------------------------|:---------------------------------------------------------------|
+| ![Pipelines mini graph](img/pipelines_mini_graph_simple.png) | ![Pipelines mini graph extended](img/pipelines_mini_graph.png) |
-## Manually executing pipelines
+### Job ordering in pipeline graphs
-Pipelines can be manually executed, with predefined or manually-specified [variables](variables/README.md).
+Job ordering depends on the type of pipeline graph. For [regular pipeline graphs](#regular-pipeline-graphs), jobs are sorted by name.
-To execute a pipeline manually:
+For [pipeline mini graphs](#pipeline-mini-graphs) ([introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760)
+in GitLab 9.0), jobs are sorted by severity and then by name.
-1. Navigate to your project's **CI/CD > Pipelines**.
-1. Click on the **Run Pipeline** button.
-1. Select the branch to run the pipeline for and enter any environment variables required for the pipeline run.
+The order of severity is:
-## Seeing pipeline status
+- failed
+- warning
+- pending
+- running
+- manual
+- scheduled
+- canceled
+- success
+- skipped
+- created
-You can find the current and historical pipeline runs under your project's
-**Pipelines** tab. Clicking on a pipeline will show the jobs that were run for
-that pipeline.
+For example:
-![Pipelines index page](img/pipelines_index.png)
+![Pipeline mini graph sorting](img/pipelines_mini_graph_sorting.png)
-## Seeing job status
+### How pipeline duration is calculated
-When you visit a single pipeline you can see the related jobs for that pipeline.
-Clicking on an individual job will show you its job trace, and allow you to
-cancel the job, retry it, or erase the job trace.
+Total running time for a given pipeline excludes retries and pending
+(queue) time.
-![Pipelines example](img/pipelines.png)
+Each job is represented as a `Period`, which consists of:
-## Seeing the failure reason for jobs
+- `Period#first` (when the job started).
+- `Period#last` (when the job finished).
-> [Introduced][ce-17782] in GitLab 10.7.
+A simple example is:
-When a pipeline fails or is allowed to fail, there are several places where you
-can quickly check the reason it failed:
+- A (1, 3)
+- B (2, 4)
+- C (6, 7)
-- **In the pipeline graph** present on the pipeline detail view.
-- **In the pipeline widgets** present in the merge requests and commit pages.
-- **In the job views** present in the global and detailed views of a job.
+In the example:
-In any case, if you hover over the failed job you can see the reason it failed.
+- A begins at 1 and ends at 3.
+- B begins at 2 and ends at 4.
+- C begins at 6 and ends at 7.
-![Pipeline detail](img/job_failure_reason.png)
+Visually it can be viewed as:
-From [GitLab 10.8][ce-17814] you can also see the reason it failed on the Job detail page.
+```text
+0 1 2 3 4 5 6 7
+ AAAAAAA
+ BBBBBBB
+ CCCC
+```
-## Pipeline graphs
+The union of A, B, and C is (1, 4) and (6, 7). Therefore, the total running time is:
-> [Introduced][ce-5742] in GitLab 8.11.
+```text
+(4 - 1) + (7 - 6) => 4
+```
-Pipelines can be complex structures with many sequential and parallel jobs.
-To make it a little easier to see what is going on, you can view a graph
-of a single pipeline and its status.
+## Configuring pipelines
-A pipeline graph can be shown in two different ways depending on what page you
-are on.
+Pipelines, and their component jobs and stages, are defined in the [`.gitlab-ci.yml`](yaml/README.md) file for each project.
----
+In particular:
-The regular pipeline graph that shows the names of the jobs of each stage can
-be found when you are on a [single pipeline page](#seeing-pipeline-status).
+- Jobs are the [basic configuration](yaml/README.html#introduction) component.
+- Stages are defined using the [`stages`](yaml/README.html#stages) keyword.
-![Pipelines example](img/pipelines.png)
+For all available configuration options, see [GitLab CI/CD Pipeline Configuration Reference](yaml/README.md).
-Then, there is the pipeline mini graph which takes less space and can give you a
-quick glance if all jobs pass or something failed. The pipeline mini graph can
-be found when you visit:
+### Settings and schedules
-- The pipelines index page.
-- A single commit page.
-- A merge request page.
+In addition to configuring jobs through `.gitlab-ci.yml`, additional configuration options are available
+through the GitLab UI:
-That way, you can see all related jobs for a single commit and the net result
-of each stage of your pipeline. This allows you to quickly see what failed and
-fix it. Stages in pipeline mini graphs are collapsible. Hover your mouse over
-them and click to expand their jobs.
+- Pipeline settings for each project. For more information, see [Pipeline settings](../user/project/pipelines/settings.md).
+- Schedules for pipelines. For more information, see [Pipeline schedules](../user/project/pipelines/schedules.md).
-| **Mini graph** | **Mini graph expanded** |
-| :------------: | :---------------------: |
-| ![Pipelines mini graph](img/pipelines_mini_graph_simple.png) | ![Pipelines mini graph extended](img/pipelines_mini_graph.png) |
+### Grouping jobs
-### Grouping similar jobs in the pipeline graph
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6242) in GitLab 8.12.
-> [Introduced][ce-6242] in GitLab 8.12.
+If you have many similar jobs, your [pipeline graph](#visualizing-pipelines) becomes long and hard
+to read.
-If you have many similar jobs, your pipeline graph becomes very long and hard
-to read. For that reason, similar jobs can automatically be grouped together.
+For that reason, similar jobs can automatically be grouped together.
If the job names are formatted in certain ways, they will be collapsed into
a single group in regular pipeline graphs (not the mini graphs).
+
You'll know when a pipeline has grouped jobs if you don't see the retry or
cancel button inside them. Hovering over them will show the number of grouped
jobs. Click to expand them.
![Grouped pipelines](img/pipelines_grouped.png)
-The basic requirements is that there are two numbers separated with one of
+#### Configuring grouping
+
+In the pipeline [configuration file](yaml/README.md), job names must include two numbers separated with one of
the following (you can even use them interchangeably):
-- A space (` `)
-- A slash (`/`)
-- A colon (`:`)
+- A space.
+- A slash (`/`).
+- A colon (`:`).
+
+NOTE: **Note:**
+More specifically, it uses [this](https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99) regular expression: `\d+[\s:\/\\]+\d+\s*`.
->**Note:**
-More specifically, [it uses][regexp] this regular expression: `\d+[\s:\/\\]+\d+\s*`.
+#### How grouping works
The jobs will be ordered by comparing those two numbers from left to right. You
usually want the first to be the index and the second the total.
For example, the following jobs will be grouped under a job named `test`:
-- `test 0 3` => `test`
-- `test 1 3` => `test`
-- `test 2 3` => `test`
+- `test 0 3`
+- `test 1 3`
+- `test 2 3`
The following jobs will be grouped under a job named `test ruby`:
-- `test 1:2 ruby` => `test ruby`
-- `test 2:2 ruby` => `test ruby`
+- `test 1:2 ruby`
+- `test 2:2 ruby`
The following jobs will be grouped under a job named `test ruby` as well:
-- `1/3 test ruby` => `test ruby`
-- `2/3 test ruby` => `test ruby`
-- `3/3 test ruby` => `test ruby`
+- `1/3 test ruby`
+- `2/3 test ruby`
+- `3/3 test ruby`
-### Manual actions from the pipeline graph
+### Pipelines for merge requests
-> [Introduced][ce-7931] in GitLab 8.15.
+GitLab supports configuring pipelines that run only for merge requests. For more information, see
+[Pipelines for merge requests](merge_request_pipelines/index.md).
-[Manual actions][manual] allow you to require manual interaction before moving
-forward with a particular job in CI. Your entire pipeline can run automatically,
-but the actual [deploy to production][env-manual] will require a click.
+### Badges
-You can do this straight from the pipeline graph. Just click on the play button
-to execute that particular job. For example, in the image below, the `production`
-stage has a job with a manual action.
+Pipeline status and test coverage report badges are available and configurable for each project.
-![Pipelines example](img/pipelines.png)
+For information on adding pipeline badges to project, see [Pipeline badges](../user/project/pipelines/settings.md#pipeline-badges).
-### Delay a particular job in the pipeline graph
+## Multi-project pipelines **[PREMIUM]**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21767) in GitLab 11.4.
+Pipelines for different projects can be combined and visualized together.
-When you do not want to run a job immediately, you can [delay the job to run after a certain period](yaml/README.md#whendelayed).
-This is especially useful for timed incremental rollout that new code is rolled out gradually.
-For example, if you start rolling out new code and users do not experience trouble, GitLab automatically completes the deployment from 0% to 100%.
-Alternatively, if you start rolling out and you noticed that a few users experience trouble with the version,
-you can stop the timed incremental rollout by canceling the pipeline, and [rolling](environments.md#rolling-back-changes) it back to the stable version.
+For more information, see [Multi-project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipelines.html).
-![Pipelines example](img/pipeline_incremental_rollout.png)
+## Working with pipelines
-### Ordering of jobs in pipeline graphs
+Generally, pipelines are executed automatically and require no intervention once created.
-**Regular pipeline graph**
+However, there are instances where you'll need to interact with pipelines. These are documented below.
-In the single pipeline page, jobs are sorted by name.
+### Manually executing pipelines
-**Mini pipeline graph**
+Pipelines can be manually executed, with predefined or manually-specified [variables](variables/README.md).
-> [Introduced][ce-9760] in GitLab 9.0.
+You might do this if the results of a pipeline (for example, a code build) is required outside the normal
+operation of the pipeline.
-In the pipeline mini graphs, the jobs are sorted first by severity and then
-by name. The order of severity is:
+To execute a pipeline manually:
-- failed
-- warning
-- pending
-- running
-- manual
-- scheduled
-- canceled
-- success
-- skipped
-- created
+1. Navigate to your project's **CI/CD > Pipelines**.
+1. Click on the **Run Pipeline** button.
+1. On the **Run Pipeline** page:
+ 1. Select the branch to run the pipeline for in the **Create for** field.
+ 1. Enter any [environment variables](variables/README.md) required for the pipeline run.
+ 1. Click the **Create pipeline** button.
-![Pipeline mini graph sorting](img/pipelines_mini_graph_sorting.png)
+The pipeline will execute the jobs as configured.
-## How the pipeline duration is calculated
+### Accessing pipelines
-Total running time for a given pipeline would exclude retries and pending
-(queue) time. We could reduce this problem down to finding the union of
-periods.
+You can find the current and historical pipeline runs under your project's
+**CI/CD > Pipelines** page. Clicking on a pipeline will show the jobs that were run for
+that pipeline.
-So each job would be represented as a `Period`, which consists of
-`Period#first` as when the job started and `Period#last` as when the
-job was finished. A simple example here would be:
+![Pipelines index page](img/pipelines_index.png)
-- A (1, 3)
-- B (2, 4)
-- C (6, 7)
+You can also access pipelines for a merge request by navigating to its **Pipelines** tab.
-Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
-C begins from 6, and ends to 7. Visually it could be viewed as:
+### Accessing individual jobs
-```
-0 1 2 3 4 5 6 7
- AAAAAAA
- BBBBBBB
- CCCC
-```
+When you access a pipeline, you can see the related jobs for that pipeline.
-The union of A, B, and C would be (1, 4) and (6, 7), therefore the
-total running time should be:
+Clicking on an individual job will show you its job trace, and allow you to:
-```
-(4 - 1) + (7 - 6) => 4
-```
+- Cancel the job.
+- Retry the job.
+- Erase the job trace.
+
+### Seeing the failure reason for jobs
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782) in GitLab 10.7.
+
+When a pipeline fails or is allowed to fail, there are several places where you
+can quickly check the reason it failed:
+
+- In the pipeline graph, on the pipeline detail view.
+- In the pipeline widgets, in the merge requests and commit pages.
+- In the job views, in the global and detailed views of a job.
+
+In any case, if you hover over the failed job you can see the reason it failed.
+
+![Pipeline detail](img/job_failure_reason.png)
+
+From [GitLab 10.8](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17814),
+you can also see the reason it failed on the Job detail page.
+
+### Manual actions from pipeline graphs
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931) in GitLab 8.15.
+
+Manual actions, configured using the [`when:manual`](yaml/README.md#whenmanual) parameter,
+allow you to require manual interaction before moving forward in the pipeline.
+
+You can do this straight from the pipeline graph. Just click on the play button
+to execute that particular job.
+
+For example, your entire pipeline could run automatically, but require manual action to
+[deploy to production](environments.md#manually-deploying-to-environments). Below, the `production`
+stage has a job with a manual action.
+
+![Pipelines example](img/pipelines.png)
+
+### Delay a job in a pipeline graph
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21767) in GitLab 11.4.
-## Badges
+When you do not want to run a job immediately, you can use the [`when:deplayed`](yaml/README.md#whendelayed) parameter to
+delay a job's execution for a certain period.
-Pipeline status and test coverage report badges are available. You can find their
-respective link in the [Pipelines settings] page.
+This is especially useful for timed incremental rollout where new code is rolled out gradually.
+
+For example, if you start rolling out new code and users:
+
+- Do not experience trouble, GitLab can automatically complete the deployment from 0% to 100%.
+- Experience trouble with the new code, you can stop the timed incremental rollout by canceling the pipeline
+ and [rolling](environments.md#rolling-back-changes) back to last stable version.
+
+![Pipelines example](img/pipeline_incremental_rollout.png)
+
+### Using the API
+
+GitLab provides API endpoints to:
+
+- Perform basic functions. For more information, see [Pipelines API](../api/pipelines.md).
+- Maintain pipeline schedules. For more information, see [Pipeline schedules API](../api/pipeline_schedules.md).
+- Trigger pipeline runs. For more information, see:
+ - [Triggering pipelines through the API](triggers/README.md).
+ - [Pipeline triggers API](../api/pipeline_triggers.md).
## Security on protected branches
@@ -276,11 +336,11 @@ The following actions are allowed on protected branches only if the user is
[allowed to merge or push](../user/project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings)
on that specific branch:
-- Run **manual pipelines** (using [Web UI](#manually-executing-pipelines) or Pipelines API).
-- Run **scheduled pipelines**.
-- Run pipelines using **triggers**.
-- Trigger **manual actions** on existing pipelines.
-- **Retry/cancel** existing jobs (using Web UI or Pipelines API).
+- Run manual pipelines (using [Web UI](#manually-executing-pipelines) or pipelines API).
+- Run scheduled pipelines.
+- Run pipelines using triggers.
+- Trigger manual actions on existing pipelines.
+- Retry/cancel existing jobs (using Web UI or pipelines API).
**Variables** marked as **protected** are accessible only to jobs that
run on protected branches, avoiding untrusted users to get unintended access to
@@ -291,19 +351,3 @@ branches, avoiding untrusted code to be executed on the protected runner and
preserving deployment keys and other credentials from being unintentionally
accessed. In order to ensure that jobs intended to be executed on protected
runners will not use regular runners, they must be tagged accordingly.
-
-[jobs]: #jobs
-[jobs-yaml]: yaml/README.md#jobs
-[manual]: yaml/README.md#whenmanual
-[env-manual]: environments.md#manually-deploying-to-environments
-[stages]: yaml/README.md#stages
-[runners]: runners/README.html
-[pipelines settings]: ../user/project/pipelines/settings.md
-[triggers]: triggers/README.md
-[ce-5742]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5742
-[ce-6242]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6242
-[ce-7931]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7931
-[ce-9760]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9760
-[ce-17782]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17782
-[ce-17814]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17814
-[regexp]: https://gitlab.com/gitlab-org/gitlab-ce/blob/2f3dc314f42dbd79813e6251792853bc231e69dd/app/models/commit_status.rb#L99
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index f4d7b9ad194..d87a13dfdc0 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -13,7 +13,7 @@ Review Apps:
- Provide an automatic live preview of changes made in a feature branch by spinning up a dynamic environment for your merge requests.
- Allow designers and product manages to see your changes without needing to check out your branch and run your changes in a sandbox environment.
-- Are fully integrated with the [GitLab DevOps LifeCycle](../../README.md#complete-devops-with-gitlab).
+- Are fully integrated with the [GitLab DevOps LifeCycle](../../README.md#the-entire-devops-lifecycle).
- Allow you to deploy your changes wherever you want.
![Review Apps Workflow](img/continuous-delivery-review-apps.svg)
diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md
index d94b472b768..2eda5d23976 100644
--- a/doc/ci/services/README.md
+++ b/doc/ci/services/README.md
@@ -10,4 +10,4 @@ be linked with your base image. Below is a list of examples you may use.
- [Using MySQL](mysql.md)
- [Using PostgreSQL](postgres.md)
- [Using Redis](redis.md)
-- [Using Other Services](../docker/using_docker_images.md#how-to-use-other-images-as-services)
+- [Using Other Services](../docker/using_docker_images.md#what-is-a-service)
diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md
index 3899b555f32..2e6d7ae94d2 100644
--- a/doc/ci/services/postgres.md
+++ b/doc/ci/services/postgres.md
@@ -31,7 +31,7 @@ Database: nice_marmot
```
If you are wondering why we used `postgres` for the `Host`, read more at
-[How is service linked to the job](../docker/using_docker_images.md#how-is-service-linked-to-the-job).
+[How services are linked to the job](../docker/using_docker_images.md#how-services-are-linked-to-the-job).
You can also use any other docker image available on [Docker Hub][hub-pg].
For example, to use PostgreSQL 9.3 the service becomes `postgres:9.3`.
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 61037360326..398b017277f 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -4,7 +4,7 @@
>
> - [Introduced](https://about.gitlab.com/2015/08/22/gitlab-7-14-released/) in GitLab 7.14.
> - GitLab 8.12 has a completely redesigned job permissions system. Read all
-> about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers).
+> about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#pipeline-triggers).
Triggers can be used to force a pipeline rerun of a specific `ref` (branch or
tag) with an API call.
@@ -17,6 +17,12 @@ The following methods of authentication are supported.
A unique trigger token can be obtained when [adding a new trigger](#adding-a-new-trigger).
+DANGER: **Danger:**
+Passing plain text tokens in public projects is a security issue. Potential
+attackers can impersonate the user that exposed their trigger token publicly in
+their `.gitlab-ci.yml` file. Use [variables](../variables/README.md#variables)
+to protect trigger tokens.
+
## Adding a new trigger
You can add a new trigger by going to your project's
@@ -53,9 +59,6 @@ The action is irreversible.
>
> - Valid refs are only the branches and tags. If you pass a commit SHA as a ref,
> it will not trigger a job.
-> - If your project is public, passing the token in plain text is probably not the
-> wisest idea, so you might want to use a
-> [variable](../variables/README.md#variables) for that purpose.
To trigger a job you need to send a `POST` request to GitLab's API endpoint:
@@ -219,7 +222,7 @@ Old triggers, created before GitLab 9.0 will be marked as legacy.
Triggers with the legacy label do not have an associated user and only have
access to the current project. They are considered deprecated and will be
removed with one of the future versions of GitLab. You are advised to
-[take ownership](#taking-ownership) of any legacy triggers.
+[take ownership](#taking-ownership-of-a-trigger) of any legacy triggers.
[ee-2017]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2017
[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 6c9831dacfd..55ecc0dccd1 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -16,7 +16,7 @@ Variables of different types can take precedence over other variables, depending
The order of precedence for variables is (from highest to lowest):
-1. [Trigger variables](../triggers/README.md#pass-job-variables-to-a-trigger) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables).
+1. [Trigger variables](../triggers/README.md#making-use-of-trigger-variables) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables).
1. Project-level [variables](#variables) or [protected variables](#protected-variables).
1. Group-level [variables](#variables) or [protected variables](#protected-variables).
1. YAML-defined [job-level variables](../yaml/README.md#variables).
@@ -46,7 +46,7 @@ version of Runner required.
NOTE: **Note:**
Starting with GitLab 9.0, we have deprecated some variables. Read the
-[9.0 Renaming](#9-0-renaming) section to find out their replacements. **You are
+[9.0 Renaming](#gitlab-90-renaming) section to find out their replacements. **You are
strongly advised to use the new variables as we will remove the old ones in
future GitLab releases.**
@@ -311,7 +311,7 @@ variables that were set, etc.
Before enabling this, you should ensure jobs are visible to
[team members only](../../user/permissions.md#project-features). You should
-also [erase](../pipelines.md#seeing-build-status) all generated job traces
+also [erase](../pipelines.md#seeing-job-status) all generated job traces
before making them visible again.
To enable debug traces, set the `CI_DEBUG_TRACE` variable to `true`:
diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md
index 1d98e8426fe..ceca4af1bee 100644
--- a/doc/ci/variables/where_variables_can_be_used.md
+++ b/doc/ci/variables/where_variables_can_be_used.md
@@ -106,9 +106,9 @@ The following variables are known as "persisted":
They are:
-- Supported for definitions where the ["Expansion place"](#gitlab-ci-yml-file) is:
+- Supported for definitions where the ["Expansion place"](#gitlab-ciyml-file) is:
- Runner.
- Script execution shell.
- Not supported:
- - For definitions where the ["Expansion place"](#gitlab-ci-yml-file) is GitLab.
+ - For definitions where the ["Expansion place"](#gitlab-ciyml-file) is GitLab.
- In the `only` and `except` [variables expressions](README.md#variables-expressions).
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 985895acce3..135de68e200 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -268,6 +268,12 @@ There are also two edge cases worth mentioning:
### `stage`
+NOTE: **Note:**
+By default, when using your own Runners, the GitLab Runner installation is set up to run only one job at a time (see the `concurrent` flag in [Runner global settings](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section) for more information).
+Jobs will run in parallel only if:
+ - Run on different Runners
+ - The Runner's `concurrent` config has been changed.
+
`stage` is defined per-job and relies on [`stages`](#stages) which is defined
globally. It allows to group jobs into different stages, and jobs of the same
`stage` are executed in `parallel`. For example:
@@ -361,10 +367,11 @@ job:
- branches@gitlab-org/gitlab-ce
except:
- master@gitlab-org/gitlab-ce
+ - release/.*@gitlab-org/gitlab-ce
```
The above example will run `job` for all branches on `gitlab-org/gitlab-ce`,
-except master.
+except `master` and those with names prefixed with `release/`.
If a job does not have an `only` rule, `only: ['branches', 'tags']` is set by
default. If it doesn't have an `except` rule, it is empty.
@@ -1696,6 +1703,11 @@ of using YAML anchors, you can use the [`extends` keyword](#extends).
See [usage examples](#include-examples).
+NOTE: **Note:**
+`.gitlab-ci.yml` configuration included by all methods is evaluated at pipeline creation.
+The configuration is a snapshot in time and persisted in the database. Any changes to
+referenced `.gitlab-ci.yml` configuration will not be reflected in GitLab until the next pipeline is created.
+
#### `include:local`
`include:local` includes a file from the same repository as `.gitlab-ci.yml`.
@@ -1750,7 +1762,7 @@ include:
```
All [nested includes](#nested-includes) will be executed in the scope of the target project,
-so it is possible to used local (relative to target project), project, remote
+so it is possible to use local (relative to target project), project, remote
or template includes.
#### `include:template`
@@ -1788,7 +1800,7 @@ or public project, or template is allowed.
#### Nested includes
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53903) in GitLab 11.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/56836) in GitLab 11.9.
Nested includes allow you to compose a set of includes.
A total of 50 includes is allowed.
@@ -2546,4 +2558,4 @@ git push -o ci.skip
[environment]: ../environments.md "CI/CD environments"
[schedules]: ../../user/project/pipelines/schedules.md "Pipelines schedules"
[variables]: ../variables/README.md "CI/CD variables"
-[push-option]: https://git-scm.com/docs/git-push#git-push--oltoptiongt
+[push-option]: https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index 5d2f5edcb18..b31870df36d 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -1 +1,5 @@
+---
+redirect_to: '../user/project/container_registry.md'
+---
+
This document was moved to [another location](../user/project/container_registry.md).
diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md
index 2f8cd37b488..c99d7011ac2 100644
--- a/doc/container_registry/troubleshooting.md
+++ b/doc/container_registry/troubleshooting.md
@@ -1 +1,5 @@
+---
+redirect_to: '../user/project/container_registry.md'
+---
+
This document was moved to [user/project/container_registry](../user/project/container_registry.md).
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index 6efed36edf0..273a7fceaf5 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -43,7 +43,7 @@ the `author` field. GitLab team members **should not**.
database records created during Cycle Analytics model spec."
- _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one.
- Example: "Fixed a typo on the search results page. (Jane Smith)"
+ Example: "Fixed a typo on the search results page."
- Performance improvements **should** have a changelog entry.
- Any change that introduces a database migration **must** have a
changelog entry.
diff --git a/doc/development/contributing/index.md b/doc/development/contributing/index.md
index f7a0fdbeb40..bfe1ef75914 100644
--- a/doc/development/contributing/index.md
+++ b/doc/development/contributing/index.md
@@ -119,6 +119,7 @@ This [documentation](merge_request_workflow.md) outlines the current merge reque
- [Merge request guidelines](merge_request_workflow.md#merge-request-guidelines)
- [Contribution acceptance criteria](merge_request_workflow.md#contribution-acceptance-criteria)
- [Definition of done](merge_request_workflow.md#definition-of-done)
+- [Dependencies](merge_request_workflow.md#dependencies)
## Style guides
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 8b14c3b20ea..5e310092a6e 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -1,91 +1,95 @@
# Merge requests
-We welcome merge requests with fixes and improvements to GitLab code, tests,
-and/or documentation. The issues that are specifically suitable for
-community contributions are listed with
-[the `Accepting merge requests` label](issue_workflow.md#label-for-community-contributors),
-but you are free to contribute to any other issue you want.
-
-Please note that if an issue is marked for the current milestone either before
-or while you are working on it, a team member may take over the merge request
+We welcome merge requests from everyone, with fixes and improvements
+to GitLab code, tests, and documentation. The issues that are specifically suitable
+for community contributions are listed with the [`Accepting merge requests`](issue_workflow.md#label-for-community-contributors)
+label, but you are free to contribute to any issue you want.
+
+Please note that if an issue is marked for the current milestone at any time, even
+when you are working on it, a GitLab Inc. team member may take over the merge request
in order to ensure the work is finished before the release date.
-If you want to add a new feature that is not labeled it is best to first create
-a feedback issue (if there isn't one already) and leave a comment asking for it
+If you want to add a new feature that is not labeled, it is best to first create
+an issue (if there isn't one already) and leave a comment asking for it
to be marked as `Accepting Merge Requests`. Please include screenshots or
-wireframes if the feature will also change the UI.
+wireframes of the proposed feature if it will also change the UI.
-Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
+Merge requests should be submitted to the appropriate project at GitLab.com, for example
+[GitLab CE](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests),
+[GitLab EE](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests),
+[GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner/merge_requests),
+[GitLab Omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests), etc.
If you are new to GitLab development (or web development in general), see the
-[I want to contribute!](index.md#i-want-to-contribute) section to get you started with
+[I want to contribute!](index.md#i-want-to-contribute) section to get started with
some potentially easy issues.
-To start with GitLab development download the [GitLab Development Kit][gdk] and
-see the [Development section](../../README.md) for some guidelines.
-
-[gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests
-[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
+To start developing GitLab, download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit)
+and see the [Development section](../../README.md) for the required guidelines.
## Merge request guidelines
-If you can, please submit a merge request with the fix or improvements
-including tests. If you don't know how to fix the issue but can write a test
-that exposes the issue we will accept that as well. In general bug fixes that
-include a regression test are merged quickly while new features without proper
-tests are least likely to receive timely feedback. The workflow to make a merge
+If you find an issue, please submit a merge request with a fix or improvement, if
+you can, and include tests. If you don't know how to fix the issue but can write a test
+that exposes the issue, we will accept that as well. In general, bug fixes that
+include a regression test are merged quickly, while new features without proper
+tests might be slower to receive feedback. The workflow to make a merge
request is as follows:
-1. Fork the project into your personal space on GitLab.com
-1. Create a feature branch, branch away from `master`
-1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code
-1. [Generate a changelog entry with `bin/changelog`][changelog]
+1. [Fork](../../workflow/forking_workflow.md#creating-a-fork) the project into
+ your personal namespace (or group) on GitLab.com.
+1. Create a feature branch in your fork (don't work off `master`).
+1. Write [tests](../rake_tasks.md#run-tests) and code.
+1. [Generate a changelog entry with `bin/changelog`](../changelog.md)
1. If you are writing documentation, make sure to follow the
- [documentation guidelines][doc-guidelines]
-1. If you have multiple commits please combine them into a few logically
- organized commits by [squashing them][git-squash]
-1. Push the commit(s) to your fork
-1. Submit a merge request (MR) to the `master` branch
- 1. Your merge request needs at least 1 approval but feel free to require more.
- For instance if you're touching backend and frontend code, it's a good idea
+ [documentation guidelines](../documentation/index.md).
+1. Follow the [commit messages guidelines](#commit-messages-guidelines).
+1. If you have multiple commits, combine them into a few logically organized
+ commits by [squashing them](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#_squashing),
+ but do not change the commit history if you're working on shared branches though.
+1. Push the commit(s) to your working branch in your fork.
+1. Submit a merge request (MR) to the `master` branch in the main GitLab project.
+ 1. Your merge request needs at least 1 approval, but feel free to require more.
+ For instance if you're touching both backend and frontend code, it's a good idea
to require 2 approvals: 1 from a backend maintainer and 1 from a frontend
- maintainer
- 1. You don't have to select any approvers, but you can if you really want
- specific people to approve your merge request
-1. The MR title should describe the change you want to make
-1. The MR description should give a motive for your change and the method you
- used to achieve it.
- 1. If you are contributing code, fill in the template already provided in the
- "Description" field.
+ maintainer.
+ 1. If you're submitting changes to documentation, you'll need approval from a technical
+ writer, based on the appropriate [product category](https://about.gitlab.com/handbook/product/categories/).
+ Only assign the MR to them when it's ready for docs review.
+ 1. You don't have to select any specific approvers, but you can if you really want
+ specific people to approve your merge request.
+1. The MR title should describe the change you want to make.
+1. The MR description should give a reason for your change.
+ 1. If you are contributing code, fill in the description according to the default
+ template already provided in the "Description" field.
1. If you are contributing documentation, choose `Documentation` from the
- "Choose a template" menu and fill in the template.
+ "Choose a template" menu and fill in the description according to the template.
1. Mention the issue(s) your merge request solves, using the `Solves #XXX` or
- `Closes #XXX` syntax to auto-close the issue(s) once the merge request will
- be merged.
-1. If you're allowed to, set a relevant milestone and labels
-1. If the MR changes the UI it should include *Before* and *After* screenshots
-1. If the MR changes CSS classes please include the list of affected pages,
- `grep css-class ./app -R`
-1. Be prepared to answer questions and incorporate feedback even if requests
- for this arrive weeks or months after your MR submission
- 1. If a discussion has been addressed, select the "Resolve discussion" button
- beneath it to mark it resolved.
-1. If your MR touches code that executes shell commands, reads or opens files or
+ `Closes #XXX` syntax to [auto-close](../../user/project/issues/automatic_issue_closing.md)
+ the issue(s) once the merge request is merged.
+1. If you're allowed to (Core team members, for example), set a relevant milestone
+ and [labels](issue_workflow.md).
+1. If the MR changes the UI, it should include *Before* and *After* screenshots.
+1. If the MR changes CSS classes, please include the list of affected pages, which
+ can be found by running `grep css-class ./app -R`.
+1. Be prepared to answer questions and incorporate feedback into your MR with new
+ commits. Once you have fully addressed a suggestion from a reviewer, click the
+ "Resolve discussion" button beneath it to mark it resolved.
+ 1. The merge request author resolves only the discussions they have fully addressed.
+ If there's an open reply or discussion, a suggestion, a question, or anything else,
+ the discussion should be left to be resolved by the reviewer.
+1. If your MR touches code that executes shell commands, reads or opens files, or
handles paths to files on disk, make sure it adheres to the
[shell command guidelines](../shell_commands.md)
1. If your code creates new files on disk please read the
[shared files guidelines](../shared_files.md).
-1. When writing commit messages please follow
- [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
- [guidelines](http://chris.beams.io/posts/git-commit/).
1. If your merge request adds one or more migrations, make sure to execute all
migrations on a fresh database before the MR is reviewed. If the review leads
- to large changes in the MR, do this again once the review is complete.
-1. For more complex migrations, write tests.
-1. Merge requests **must** adhere to the [merge request performance
- guidelines](../merge_request_performance_guidelines.md).
-1. For tests that use Capybara or PhantomJS, see this [article on how
- to write reliable asynchronous tests](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara).
+ to large changes in the MR, execute the migrations again once the review is complete.
+1. Write tests for more complex migrations.
+1. Merge requests **must** adhere to the [merge request performance guidelines](../merge_request_performance_guidelines.md).
+1. For tests that use Capybara, read
+ [how to write reliable, asynchronous integration tests](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara).
1. If your merge request introduces changes that require additional steps when
installing GitLab from source, add them to `doc/install/installation.md` in
the same merge request.
@@ -95,109 +99,117 @@ request is as follows:
instructions are specific to a version, add them to the "Version specific
upgrading instructions" section.
-Please keep the change in a single MR **as small as possible**. If you want to
-contribute a large feature think very hard what the minimum viable change is.
-Can you split the functionality? Can you only submit the backend/API code? Can
-you start with a very simple UI? Can you do part of the refactor? The increased
-reviewability of small MRs that leads to higher code quality is more important
-to us than having a minimal commit log. The smaller an MR is the more likely it
-is it will be merged (quickly). After that you can send more MRs to enhance it.
-The ['How to get faster PR reviews' document of Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/faster_reviews.md) also has some great points regarding this.
-
-For examples of feedback on merge requests please look at already
-[closed merge requests][closed-merge-requests]. If you would like quick feedback
-on your merge request feel free to mention someone from the [core team] or one
-of the [Merge request coaches][team].
-Please ensure that your merge request meets the contribution acceptance criteria.
-
-When having your code reviewed and when reviewing merge requests please take the
-[code review guidelines](../code_review.md) into account.
-
-[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
-[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
-[team]: https://about.gitlab.com/team/
+If you would like quick feedback on your merge request feel free to mention someone
+from the [core team](https://about.gitlab.com/community/core-team/) or one of the
+[merge request coaches](https://about.gitlab.com/team/). When having your code reviewed
+and when reviewing merge requests, please keep the [code review guidelines](../code_review.md)
+in mind.
+
+### Keep it simple
+
+*Live by smaller iterations.* Please keep the amount of changes in a single MR **as small as possible**.
+If you want to contribute a large feature, think very carefully about what the
+[minimum viable change](https://about.gitlab.com/handbook/product/#the-minimally-viable-change)
+is. Can you split the functionality into two smaller MRs? Can you submit only the
+backend/API code? Can you start with a very simple UI? Can you do just a part of the
+refactor?
+
+Small MRs which are more easily reviewed, lead to higher code quality which is
+more important to GitLab than having a minimal commit log. The smaller an MR is,
+the more likely it will be merged quickly. After that you can send more MRs to
+enhance and expand the feature. The [How to get faster PR reviews](https://github.com/kubernetes/kubernetes/blob/release-1.5/docs/devel/faster_reviews.md)
+document from the Kubernetes team also has some great points regarding this.
+
+### Commit messages guidelines
+
+When writing commit messages, please follow the guidelines below:
+
+- The commit subject must contain at least 3 words.
+- The commit subject should ideally contain up to 50 characters,
+and must not be longer than 72 characters.
+- The commit subject must start with a capital letter.
+- The commit subject must not end with a period.
+- The commit subject and body must be separated by a blank line.
+- The commit body must not contain more than 72 characters per line.
+- Commits that change 30 or more lines across at least 3 files must
+describe these changes in the commit body.
+- The commit subject or body must not contain Emojis.
+- Use issues and merge requests' full URLs instead of short references,
+as they are displayed as plain text outside of GitLab.
+- The merge request must not contain more than 10 commit messages.
+
+If the guidelines are not met, the MR will not pass the
+[Danger checks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/danger/commit_messages/Dangerfile).
+For more information see [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/).
## Contribution acceptance criteria
-1. The change is as small as possible
+To make sure that your merge request can be approved, please ensure that it meets
+the contribution acceptance criteria below:
+
+1. The change is as small as possible.
1. Include proper tests and make all tests pass (unless it contains a test
exposing a bug in existing code). Every new class should have corresponding
unit tests, even if the class is exercised at a higher level, such as a feature test.
-1. If you suspect a failing CI build is unrelated to your contribution, you may
- try and restart the failing CI job or ask a developer to fix the
- aforementioned failing test
-1. Your MR initially contains a single commit (please use `git rebase -i` to
- squash commits)
-1. Your changes can merge without problems (if not please rebase if you're the
- only one working on your feature branch, otherwise, merge `master`)
-1. Does not break any existing functionality
-1. Fixes one specific issue or implements one specific feature (do not combine
- things, send separate merge requests if needed)
-1. Migrations should do only one thing (e.g., either create a table, move data
- to a new table or remove an old table) to aid retrying on failure
-1. Keeps the GitLab code base clean and well structured
-1. Contains functionality we think other users will benefit from too
-1. Doesn't add configuration options or settings options since they complicate
- making and testing future changes
-1. Changes do not adversely degrade performance.
- - Avoid repeated polling of endpoints that require a significant amount of overhead
- - Check for N+1 queries via the SQL log or [`QueryRecorder`](https://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
- - Avoid repeated access of filesystem
-1. If you need polling to support real-time features, please use
- [polling with ETag caching][polling-etag].
-1. Changes after submitting the merge request should be in separate commits
- (no squashing).
-1. It conforms to the [style guides](style_guides.md) and the following:
- - If your change touches a line that does not follow the style, modify the
- entire line to follow it. This prevents linting tools from generating warnings.
- - Don't touch neighbouring lines. As an exception, automatic mass
- refactoring modifications may leave style non-compliant.
-1. If the merge request adds any new libraries (gems, JavaScript libraries,
- etc.), they should conform to our [Licensing guidelines][license-finder-doc].
- See the instructions in that document for help if your MR fails the
- "license-finder" test with a "Dependencies that need approval" error.
-1. The merge request meets the [definition of done](#definition-of-done).
-
-[license-finder-doc]: ../licensing.md
-[polling-etag]: ../polling.md
+ - If a failing CI build seems to be unrelated to your contribution, you can try
+ restarting the failing CI job, rebasing from master to bring in updates that
+ may resolve the failure, or if it has not been fixed yet, ask a developer to
+ help you fix the test.
+1. The MR initially contains a a few logically organized commits.
+1. The changes can merge without problems. If not, you should rebase if you're the
+ only one working on your feature branch, otherwise merge `master`.
+1. Only one specific issue is fixed or one specific feature is implemented. Do not
+ combine things; send separate merge requests for each issue or feature.
+1. Migrations should do only one thing (e.g., create a table, move data to a new
+ table, or remove an old table) to aid retrying on failure.
+1. Contains functionality that other users will benefit from.
+1. Doesn't add configuration options or settings options since they complicate making
+ and testing future changes.
+1. Changes do not degrade performance:
+ - Avoid repeated polling of endpoints that require a significant amount of overhead.
+ - Check for N+1 queries via the SQL log or [`QueryRecorder`](../merge_request_performance_guidelines.md).
+ - Avoid repeated access of the filesystem.
+ - Use [polling with ETag caching](../polling.md) if needed to support real-time features.
+1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.),
+ they should conform to our [Licensing guidelines](../licensing.md). See those
+ instructions for help if the "license-finder" test fails with a
+ `Dependencies that need approval` error. Also, make the reviewer aware of the new
+ library and explain why you need it.
+1. The merge request meets GitLab's [definition of done](#definition-of-done), below.
## Definition of done
If you contribute to GitLab please know that changes involve more than just
-code. We have the following [definition of done][definition-of-done]. Please ensure you support
-the feature you contribute through all of these steps.
-
-1. Description explaining the relevancy (see following item)
-1. Working and clean code that is commented where needed
-1. [Unit, integration, and system tests][testing] that pass on the CI server
-1. Performance/scalability implications have been considered, addressed, and tested
-1. [Documented][doc-guidelines] in the `/doc` directory
-1. [Changelog entry added][changelog], if necessary
-1. Reviewed by UX/FE/BE and any concerns are addressed
-1. Merged by a project maintainer
-1. Added to the release blog article, if relevant
-1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/), if relevant
-1. Community questions answered
-1. Answers to questions radiated (in docs/wiki/support etc.)
-1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-at-the-system-level-aka-end-to-end-tests) added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams) with any questions
+code. We use the following [definition of done](https://www.agilealliance.org/glossary/definition-of-done).
+Your contribution is not *done* until you have made sure it meets all of these
+requirements.
+
+1. Clear description explaining the relevancy of the contribution.
+1. Working and clean code that is commented where needed.
+1. [Unit, integration, and system tests](../testing_guide/index.md) that all pass
+ on the CI server.
+1. Performance/scalability implications have been considered, addressed, and tested.
+1. [Documented](../documentation/index.md) in the `/doc` directory.
+1. [Changelog entry added](../changelog.md), if necessary.
+1. Reviewed by relevant (UX/FE/BE/tech writing) reviewers and all concerns are addressed.
+1. Merged by a project maintainer.
+1. Added to the [release post](https://about.gitlab.com/handbook/marketing/blog/release-posts/),
+ if relevant.
+1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml), if relevant.
+1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-at-the-system-level-aka-end-to-end-tests)
+ added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams)
+ with any questions.
+
+## Dependencies
If you add a dependency in GitLab (such as an operating system package) please
-consider updating the following and note the applicability of each in your
-merge request:
-
-1. Note the addition in the release blog post (create one if it doesn't exist yet) <https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/>
-1. Upgrade guide, for example <https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/7.5-to-7.6.md>
-1. Installation guide <https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies>
-1. GitLab Development Kit <https://gitlab.com/gitlab-org/gitlab-development-kit>
-1. Test suite <https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/prepare_build.sh>
-1. Omnibus package creator <https://gitlab.com/gitlab-org/omnibus-gitlab>
-
-[definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html
-[testing]: ../testing_guide/index.md
-
----
-
-[Return to Contributing documentation](index.md)
-
-[changelog]: ../changelog.md "Generate a changelog entry"
-[doc-guidelines]: ../documentation/index.md "Documentation guidelines"
+consider updating the following, and note the applicability of each in your merge
+request:
+
+1. Note the addition in the [release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/)
+ (create one if it doesn't exist yet).
+1. [The upgrade guide](../../update/upgrading_from_source.md).
+1. The [GitLab Installation Guide](../../install/installation.md#1-packages-and-dependencies).
+1. The [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
+1. The [CI environment preparation](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/prepare_build.sh).
+1. The [Omnibus package creator](https://gitlab.com/gitlab-org/omnibus-gitlab).
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 0c51d3832aa..c0386290785 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -80,11 +80,10 @@ yield a useful result, and ensuring content is helpful and easy to consume.
## Text
-- Split up long lines (wrap text), this makes it much easier to review and edit. Only
- double line breaks are shown as a full line break by creating new paragraphs.
- 80-100 characters is the recommended line length.
+- Splitting long lines (preferably up to 100 characters) can make it easier to provide feedback on small chunks of text.
+- Insert an empty line for new paragraphs.
- Use sentence case for titles, headings, labels, menu items, and buttons.
-- Jump a line between different markups (e.g., after every paragraph, header, list, etc). Example:
+- Insert an empty line between different markups (e.g., after every paragraph, header, list, etc). Example:
```md
## Header
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 4b60ec80cb8..7b54fa6289c 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -52,7 +52,7 @@ The first thing you should do before writing any code is to design the state.
Often we need to provide data from haml to our Vue application. Let's store it in the state for better access.
```javascript
- export default {
+ export default () => ({
endpoint: null,
isLoading: false,
@@ -62,7 +62,7 @@ Often we need to provide data from haml to our Vue application. Let's store it i
errorAddingUser: false,
users: [],
- };
+ });
```
#### Access `state` properties
diff --git a/doc/development/new_fe_guide/style/javascript.md b/doc/development/new_fe_guide/style/javascript.md
index 7985b893c9e..3019eaa089c 100644
--- a/doc/development/new_fe_guide/style/javascript.md
+++ b/doc/development/new_fe_guide/style/javascript.md
@@ -1,208 +1,196 @@
# JavaScript style guide
-We use [Airbnb's JavaScript Style Guide][airbnb-style-guide] and it's accompanying linter to manage most of our JavaScript style guidelines.
+We use [Airbnb's JavaScript Style Guide](https://github.com/airbnb/javascript) and it's accompanying
+linter to manage most of our JavaScript style guidelines.
-In addition to the style guidelines set by Airbnb, we also have a few specific rules listed below.
+In addition to the style guidelines set by Airbnb, we also have a few specific rules
+listed below.
> **Tip:**
You can run eslint locally by running `yarn eslint`
-## Arrays
+## Avoid forEach
+
+Avoid forEach when mutating data. Use `map`, `reduce` or `filter` instead of `forEach`
+when mutating data. This will minimize mutations in functions,
+which aligns with [Airbnb's style guide](https://github.com/airbnb/javascript#testing--for-real).
+
+```javascript
+// bad
+users.forEach((user, index) => {
+ user.id = index;
+});
+
+// good
+const usersWithId = users.map((user, index) => {
+ return Object.assign({}, user, { id: index });
+});
+```
+
+## Limit number of parameters
+
+If your function or method has more than 3 parameters, use an object as a parameter
+instead.
+
+```javascript
+// bad
+function a(p1, p2, p3) {
+ // ...
+};
+
+// good
+function a(p) {
+ // ...
+};
+```
+
+## Avoid side effects in constructors
+
+Avoid making asynchronous calls, API requests or DOM manipulations in the `constructor`.
+Move them into separate functions instead. This will make tests easier to write and
+code easier to maintain.
-<a name="avoid-foreach"></a><a name="1.1"></a>
+```javascript
+// bad
+class myClass {
+ constructor(config) {
+ this.config = config;
+ axios.get(this.config.endpoint)
+ }
+}
-- [1.1](#avoid-foreach) **Avoid ForEach when mutating data** Use `map`, `reduce` or `filter` instead of `forEach` when mutating data. This will minimize mutations in functions ([which is aligned with Airbnb's style guide][airbnb-minimize-mutations])
+// good
+class myClass {
+ constructor(config) {
+ this.config = config;
+ }
- ```
- // bad
- users.forEach((user, index) => {
- user.id = index;
- });
+ makeRequest() {
+ axios.get(this.config.endpoint)
+ }
+}
+const instance = new myClass();
+instance.makeRequest();
- // good
- const usersWithId = users.map((user, index) => {
- return Object.assign({}, user, { id: index });
- });
- ```
+```
-## Functions
+## Avoid classes to handle DOM events
-<a name="limit-params"></a><a name="2.1"></a>
+If the only purpose of the class is to bind a DOM event and handle the callback, prefer
+using a function.
-- [2.1](#limit-params) **Limit number of parameters** If your function or method has more than 3 parameters, use an object as a parameter instead.
+```javascript
+// bad
+class myClass {
+ constructor(config) {
+ this.config = config;
+ }
- ```
- // bad
- function a(p1, p2, p3) {
- // ...
- };
+ init() {
+ document.addEventListener('click', () => {});
+ }
+}
- // good
- function a(p) {
- // ...
- };
- ```
+// good
-## Classes & constructors
+const myFunction = () => {
+ document.addEventListener('click', () => {
+ // handle callback here
+ });
+}
+```
-<a name="avoid-constructor-side-effects"></a><a name="3.1"></a>
+## Pass element container to constructor
-- [3.1](#avoid-constructor-side-effects) **Avoid side effects in constructors** Avoid making some operations in the `constructor`, such as asynchronous calls, API requests and DOM manipulations. Prefer moving them into separate functions. This will make tests easier to write and code easier to maintain.
+When your class manipulates the DOM, receive the element container as a parameter.
+This is more maintainable and performant.
- ```javascript
- // bad
- class myClass {
- constructor(config) {
- this.config = config;
- axios.get(this.config.endpoint)
- }
- }
+```javascript
+// bad
+class a {
+ constructor() {
+ document.querySelector('.b');
+ }
+}
- // good
- class myClass {
- constructor(config) {
- this.config = config;
- }
+// good
+class a {
+ constructor(options) {
+ options.container.querySelector('.b');
+ }
+}
+```
- makeRequest() {
- axios.get(this.config.endpoint)
- }
- }
- const instance = new myClass();
- instance.makeRequest();
+## Use ParseInt
- ```
+Use `ParseInt` when converting a numeric string into a number.
-<a name="avoid-classes-to-handle-dom-events"></a><a name="3.2"></a>
+```javascript
+// bad
+Number('10')
-- [3.2](#avoid-classes-to-handle-dom-events) **Avoid classes to handle DOM events** If the only purpose of the class is to bind a DOM event and handle the callback, prefer using a function.
+// good
+parseInt('10', 10);
+```
- ```
- // bad
- class myClass {
- constructor(config) {
- this.config = config;
- }
+## CSS Selectors - Use `js-` prefix
- init() {
- document.addEventListener('click', () => {});
- }
- }
+If a CSS class is only being used in JavaScript as a reference to the element, prefix
+the class name with `js-`.
- // good
+```html
+// bad
+<button class="add-user"></button>
- const myFunction = () => {
- document.addEventListener('click', () => {
- // handle callback here
- });
- }
- ```
+// good
+<button class="js-add-user"></button>
+```
-<a name="element-container"></a><a name="3.3"></a>
+## Absolute vs relative paths for modules
-- [3.3](#element-container) **Pass element container to constructor** When your class manipulates the DOM, receive the element container as a parameter.
- This is more maintainable and performant.
+Use relative paths if the module you are importing is less than two levels up.
- ```
- // bad
- class a {
- constructor() {
- document.querySelector('.b');
- }
- }
+```javascript
+// bad
+import GitLabStyleGuide from '~/guides/GitLabStyleGuide';
- // good
- class a {
- constructor(options) {
- options.container.querySelector('.b');
- }
- }
- ```
+// good
+import GitLabStyleGuide from '../GitLabStyleGuide';
+```
-## Type Casting & Coercion
+If the module you are importing is two or more levels up, use an absolute path instead:
-<a name="use-parseint"></a><a name="4.1"></a>
+```javascript
+// bad
+import GitLabStyleGuide from '../../../guides/GitLabStyleGuide';
-- [4.1](#use-parseint) **Use ParseInt** Use `ParseInt` when converting a numeric string into a number.
+// good
+import GitLabStyleGuide from '~/GitLabStyleGuide';
+```
- ```
- // bad
- Number('10')
+Additionally, **do not add to global namespace**.
- // good
- parseInt('10', 10);
- ```
+## Do not use `DOMContentLoaded` in non-page modules
-## CSS Selectors
+Imported modules should act the same each time they are loaded. `DOMContentLoaded`
+events are only allowed on modules loaded in the `/pages/*` directory because those
+are loaded dynamically with webpack.
-<a name="use-js-prefix"></a><a name="5.1"></a>
+## Avoid XSS
-- [5.1](#use-js-prefix) **Use js prefix** If a CSS class is only being used in JavaScript as a reference to the element, prefix the class name with `js-`
+Do not use `innerHTML`, `append()` or `html()` to set content. It opens up too many
+vulnerabilities.
- ```
- // bad
- <button class="add-user"></button>
+## Disabling ESLint in new files
- // good
- <button class="js-add-user"></button>
- ```
+Do not disable ESLint when creating new files. Existing files may have existing rules
+disabled due to legacy compatibility reasons but they are in the process of being refactored.
-## Modules
+Do not disable specific ESLint rules. Due to technical debt, you may disable the following
+rules only if you are invoking/instantiating existing code modules.
-<a name="use-absolute-paths"></a><a name="6.1"></a>
+ - [no-new](http://eslint.org/docs/rules/no-new)
+ - [class-method-use-this](http://eslint.org/docs/rules/class-methods-use-this)
-- [6.1](#use-absolute-paths) **Use absolute paths for nearby modules** Use absolute paths if the module you are importing is less than two levels up.
-
- ```
- // bad
- import GitLabStyleGuide from '~/guides/GitLabStyleGuide';
-
- // good
- import GitLabStyleGuide from '../GitLabStyleGuide';
- ```
-
-<a name="use-relative-paths"></a><a name="6.2"></a>
-
-- [6.2](#use-relative-paths) **Use relative paths for distant modules** If the module you are importing is two or more levels up, use a relative path instead of an absolute path.
-
- ```
- // bad
- import GitLabStyleGuide from '../../../guides/GitLabStyleGuide';
-
- // good
- import GitLabStyleGuide from '~/GitLabStyleGuide';
- ```
-
-<a name="global-namespace"></a><a name="6.3"></a>
-
-- [6.3](#global-namespace) **Do not add to global namespace**
-
-<a name="domcontentloaded"></a><a name="6.4"></a>
-
-- [6.4](#domcontentloaded) **Do not use DOMContentLoaded in non-page modules** Imported modules should act the same each time they are loaded. `DOMContentLoaded` events are only allowed on modules loaded in the `/pages/*` directory because those are loaded dynamically with webpack.
-
-## Security
-
-<a name="avoid-xss"></a><a name="7.1"></a>
-
-- [7.1](#avoid-xss) **Avoid XSS** Do not use `innerHTML`, `append()` or `html()` to set content. It opens up too many vulnerabilities.
-
-## ESLint
-
-<a name="disable-eslint-file"></a><a name="8.1"></a>
-
-- [8.1](#disable-eslint-file) **Disabling ESLint in new files** Do not disable ESLint when creating new files. Existing files may have existing rules disabled due to legacy compatibility reasons but they are in the process of being refactored.
-
-<a name="disable-eslint-rule"></a><a name="8.2"></a>
-
-- [8.2](#disable-eslint-rule) **Disabling ESLint rule** Do not disable specific ESLint rules. Due to technical debt, you may disable the following rules only if you are invoking/instantiating existing code modules
-
- - [no-new][no-new]
- - [class-method-use-this][class-method-use-this]
-
-> Note: Disable these rules on a per line basis. This makes it easier to refactor in the future. E.g. use `eslint-disable-next-line` or `eslint-disable-line`
-
-[airbnb-style-guide]: https://github.com/airbnb/javascript
-[airbnb-minimize-mutations]: https://github.com/airbnb/javascript#testing--for-real
-[no-new]: http://eslint.org/docs/rules/no-new
-[class-method-use-this]: http://eslint.org/docs/rules/class-methods-use-this
+> Note: Disable these rules on a per line basis. This makes it easier to refactor
+ in the future. E.g. use `eslint-disable-next-line` or `eslint-disable-line`.
diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md
index a239dc84a1c..daa0714aec3 100644
--- a/doc/development/testing_guide/end_to_end_tests.md
+++ b/doc/development/testing_guide/end_to_end_tests.md
@@ -25,10 +25,10 @@ You can find these nightly pipelines at [gitlab-org/quality/staging/pipelines][q
### Testing code in merge requests
-It is possible to run end-to-end tests (eventually being run within a
-[GitLab QA pipeline][gitlab-qa-pipelines]) for a merge request by triggering
-the `package-and-qa` manual action in the `test` stage, that should be present
-in a merge request widget (unless the merge request is from a fork).
+It is possible to run end-to-end tests for a merge request, eventually being run in
+a pipeline in the [`gitlab-qa`](https://gitlab.com/gitlab-org/gitlab-qa/) project,
+by triggering the `package-and-qa` manual action in the `test` stage (which should
+be present in a merge request widget, unless the merge request comes from a fork).
Manual action that starts end-to-end tests is also available in merge requests
in [Omnibus GitLab][omnibus-gitlab].
@@ -81,7 +81,6 @@ you can find an issue you would like to work on in
[omnibus-gitlab]: https://gitlab.com/gitlab-org/omnibus-gitlab
[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
[gitlab-qa-readme]: https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md
-[gitlab-qa-pipelines]: https://gitlab.com/gitlab-org/gitlab-qa/pipelines
[quality-nightly-pipelines]: https://gitlab.com/gitlab-org/quality/nightly/pipelines
[quality-staging-pipelines]: https://gitlab.com/gitlab-org/quality/staging/pipelines
[gitlab-qa-architecture]: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/architecture.md
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 5b66e513a76..d4a2ac246c4 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -15,7 +15,7 @@ information on general testing practices at GitLab.
## Jest
-GitLab has started to migrate tests to the (Jest)[https://jestjs.io]
+GitLab has started to migrate tests to the [Jest](https://jestjs.io)
testing framework. You can read a [detailed evaluation](https://gitlab.com/gitlab-org/gitlab-ce/issues/49171)
of Jest compared to our use of Karma and Jasmine. In summary, it will allow us
to improve the performance and consistency of our frontend tests.
@@ -35,15 +35,16 @@ If your test exceeds that time, it will fail.
If you cannot improve the performance of the tests, you can increase the timeout
for a specific test using
-[`jest.setTimeout`](https://jestjs.io/docs/en/jest-object#jestsettimeouttimeout).
+[`setTestTimeout`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/frontend/helpers/timeout.js).
```javascript
-beforeAll(() => {
- jest.setTimeout(500);
-});
+import { setTestTimeout } from 'helpers/timeout';
describe('Component', () => {
- // ...
+ it('does something amazing', () => {
+ setTestTimeout(500);
+ // ...
+ });
});
```
@@ -281,25 +282,6 @@ Information on setting up and running RSpec integration tests with
## Gotchas
-### Errors due to use of unsupported JavaScript features
-
-Similar errors will be thrown if you're using JavaScript features not yet
-supported by the PhantomJS test runner which is used for both Karma and RSpec
-tests. We polyfill some JavaScript objects for older browsers, but some
-features are still unavailable:
-
-- Array.from
-- Array.first
-- Async functions
-- Generators
-- Array destructuring
-- For..Of
-- Symbol/Symbol.iterator
-- Spread
-
-Until these are polyfilled appropriately, they should not be used. Please
-update this list with additional unsupported features.
-
### RSpec errors due to JavaScript
By default RSpec unit tests will not run JavaScript in the headless browser
diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md
index da7853d7d34..8fecdc6948e 100644
--- a/doc/gitlab-basics/create-your-ssh-keys.md
+++ b/doc/gitlab-basics/create-your-ssh-keys.md
@@ -1,35 +1,22 @@
-# How to create your SSH Keys
+# How to create your SSH keys
-1. Go to your [command line](start-using-git.md) and follow the [instructions](../ssh/README.md) to generate your SSH key pair.
-
-1. Log in to GitLab with your credentials.
-1. In the upper-right corner, click your avatar and then click **Settings**.
-
- ![Profile settings dropdown](img/profile_settings.png)
-
-1. Navigate to the **SSH keys** tab.
-
- ![SSH Keys](img/profile_settings_ssh_keys.png)
-
-1. Paste your **public** key that you generated in the first step in the 'Key'
- box.
+This topic describes how to create SSH keys. You do this to use Git over SSH instead of Git over HTTP.
- ![Paste SSH public key](img/profile_settings_ssh_keys_paste_pub.png)
+## Creating your SSH keys
+1. Go to your [command line](start-using-git.md) and follow the [instructions](../ssh/README.md) to generate your SSH key pair.
+1. Log in to GitLab.
+1. In the upper-right corner, click your avatar and select **Settings**.
+1. On the **User Settings** menu, select **SSH keys**.
+1. Paste the **public** key generated in the first step in the **Key**
+ text field.
1. Optionally, give it a descriptive title so that you can recognize it in the
event you add multiple keys.
-
- ![SSH key title](img/profile_settings_ssh_keys_title.png)
-
-1. Finally, click **Add key** to add it to GitLab. You will be able to see
+1. Finally, click the **Add key** button to add it to GitLab. You will be able to see
its fingerprint, title, and creation date.
![SSH key single page](img/profile_settings_ssh_keys_single_key.png)
->**Note:**
-Once you add a key, you cannot edit it, only remove it. In case the paste
-didn't work, you will have to remove the offending key and re-add it.
-
----
-
-Congratulations! You are now ready to use Git over SSH, instead of Git over HTTP!
+NOTE: **Note:**
+Once you add a key, you cannot edit it. If the paste
+didn't work, you need to remove the offending key and re-add it.
diff --git a/doc/gitlab-basics/img/profile_settings.png b/doc/gitlab-basics/img/profile_settings.png
deleted file mode 100644
index b91b698fb18..00000000000
--- a/doc/gitlab-basics/img/profile_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys.png b/doc/gitlab-basics/img/profile_settings_ssh_keys.png
deleted file mode 100644
index 8ac603a2af9..00000000000
--- a/doc/gitlab-basics/img/profile_settings_ssh_keys.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
deleted file mode 100644
index 0b1c64a72f3..00000000000
--- a/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
deleted file mode 100644
index 02ca0bf7478..00000000000
--- a/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
+++ /dev/null
Binary files differ
diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md
index 1d5e5dd6e15..fb939ff8aac 100644
--- a/doc/hooks/custom_hooks.md
+++ b/doc/hooks/custom_hooks.md
@@ -1,3 +1,7 @@
+---
+redirect_to: '../administration/custom_hooks.md'
+---
+
# Custom Git Hooks
This document was moved to [administration/custom_hooks.md](../administration/custom_hooks.md).
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
index db0f03f2c98..9544983974f 100644
--- a/doc/incoming_email/README.md
+++ b/doc/incoming_email/README.md
@@ -1 +1,5 @@
+---
+redirect_to: '../administration/reply_by_email.md'
+---
+
This document was moved to [administration/reply_by_email](../administration/reply_by_email.md).
diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md
index 90833238ac5..a7192325229 100644
--- a/doc/incoming_email/postfix.md
+++ b/doc/incoming_email/postfix.md
@@ -1 +1,5 @@
+---
+redirect_to: '../administration/reply_by_email_postfix_setup.md'
+---
+
This document was moved to [administration/reply_by_email_postfix_setup](../administration/reply_by_email_postfix_setup.md).
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 8ab7189c2a6..9a6c2ce1976 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -207,6 +207,9 @@ We support the current and the previous major release of:
- Microsoft Edge
- Internet Explorer 11
+The browser vendors release regular minor version updates with important bug fixes and security updates.
+Support is only provided for the current minor version of the major version you are running.
+
Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
Note: We do not support running GitLab with JavaScript disabled in the browser and have no plans of supporting that
diff --git a/doc/integration/chat_commands.md b/doc/integration/chat_commands.md
index 2856992ee25..1a4fb46046d 100644
--- a/doc/integration/chat_commands.md
+++ b/doc/integration/chat_commands.md
@@ -1 +1,5 @@
+---
+redirect_to: 'slash_commands.md'
+---
+
This document was moved to [integration/slash_commands.md](slash_commands.md).
diff --git a/doc/integration/crowd.md b/doc/integration/crowd.md
index 2bc526dc3db..30e3e888b29 100644
--- a/doc/integration/crowd.md
+++ b/doc/integration/crowd.md
@@ -1 +1,5 @@
+---
+redirect_to: '../administration/auth/crowd.md'
+---
+
This document was moved to [`administration/auth/crowd`](../administration/auth/crowd.md).
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
index b6923f74e28..c09fde08326 100644
--- a/doc/integration/jira.md
+++ b/doc/integration/jira.md
@@ -1 +1,5 @@
+---
+redirect_to: '../user/project/integrations/jira.md'
+---
+
This document was moved to [integrations/jira](../user/project/integrations/jira.md).
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index 242890af981..76c124d2ce9 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -1 +1,5 @@
+---
+redirect_to: '../administration/auth/ldap.md'
+---
+
This document was moved to [`administration/auth/ldap`](../administration/auth/ldap.md).
diff --git a/doc/integration/slack.md b/doc/integration/slack.md
index 8cd151fbf95..f84ab769218 100644
--- a/doc/integration/slack.md
+++ b/doc/integration/slack.md
@@ -1 +1,5 @@
+---
+redirect_to: '../project_services/slack.md'
+---
+
This document was moved to [project_services/slack.md](../project_services/slack.md).
diff --git a/doc/logs/logs.md b/doc/logs/logs.md
index a2eca62d691..0cb092c85fd 100644
--- a/doc/logs/logs.md
+++ b/doc/logs/logs.md
@@ -1 +1,5 @@
+---
+redirect_to: '../administration/logs.md'
+---
+
This document was moved to [administration/logs.md](../administration/logs.md).
diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md
index 6cf93c33ec2..b611fa388b4 100644
--- a/doc/monitoring/health_check.md
+++ b/doc/monitoring/health_check.md
@@ -1 +1,5 @@
+---
+redirect_to: '../user/admin_area/monitoring/health_check.md'
+---
+
This document was moved to [user/admin_area/monitoring/health_check](../user/admin_area/monitoring/health_check.md).
diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md
index 19d46135930..233a12ebd6f 100644
--- a/doc/monitoring/performance/gitlab_configuration.md
+++ b/doc/monitoring/performance/gitlab_configuration.md
@@ -1 +1,5 @@
+---
+redirect_to: '../../administration/monitoring/performance/gitlab_configuration.md'
+---
+
This document was moved to [administration/monitoring/performance/gitlab_configuration](../../administration/monitoring/performance/gitlab_configuration.md).
diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md
index 0d4be02ff5f..f4e3561a19f 100644
--- a/doc/monitoring/performance/grafana_configuration.md
+++ b/doc/monitoring/performance/grafana_configuration.md
@@ -1 +1,5 @@
+---
+redirect_to: '../../administration/monitoring/performance/grafana_configuration.md'
+---
+
This document was moved to [administration/monitoring/performance/grafana_configuration](../../administration/monitoring/performance/grafana_configuration.md).
diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md
index 15fd275e916..ae5f4c7e9df 100644
--- a/doc/monitoring/performance/influxdb_configuration.md
+++ b/doc/monitoring/performance/influxdb_configuration.md
@@ -1 +1,5 @@
+---
+redirect_to: '../../administration/monitoring/performance/influxdb_configuration.md'
+---
+
This document was moved to [administration/monitoring/performance/influxdb_configuration](../../administration/monitoring/performance/influxdb_configuration.md).
diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md
index e53f9701dc3..57fb74cb6cd 100644
--- a/doc/monitoring/performance/influxdb_schema.md
+++ b/doc/monitoring/performance/influxdb_schema.md
@@ -1 +1,5 @@
+---
+redirect_to: '../../administration/monitoring/performance/influxdb_schema.md'
+---
+
This document was moved to [administration/monitoring/performance/influxdb_schema](../../administration/monitoring/performance/influxdb_schema.md).
diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md
index 4d6f02b6547..e23eabd5f40 100644
--- a/doc/monitoring/performance/introduction.md
+++ b/doc/monitoring/performance/introduction.md
@@ -1 +1,5 @@
+---
+redirect_to: '../../administration/monitoring/performance/index.md'
+---
+
This document was moved to [administration/monitoring/performance/introduction](../../administration/monitoring/performance/index.md).
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index 78d67aeec78..85923a40b2c 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -1,3 +1,7 @@
+---
+redirect_to: '../user/permissions.md'
+---
+
# Permissions
This document was moved to [user/permissions.md](../user/permissions.md).
diff --git a/doc/profile/README.md b/doc/profile/README.md
index fda6d85a84c..4932cf33b87 100644
--- a/doc/profile/README.md
+++ b/doc/profile/README.md
@@ -1 +1,5 @@
+---
+redirect_to: '../user/profile/index.md'
+---
+
This document was moved to [user/profile/account](../user/profile/index.md).
diff --git a/doc/profile/preferences.md b/doc/profile/preferences.md
index cc16f3afe41..cf99bd61f5d 100644
--- a/doc/profile/preferences.md
+++ b/doc/profile/preferences.md
@@ -1 +1,5 @@
+---
+redirect_to: '../user/profile/preferences.md'
+---
+
This document was moved to [another location](../user/profile/preferences.md).
diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md
index 60918a0339c..453ac833f59 100644
--- a/doc/profile/two_factor_authentication.md
+++ b/doc/profile/two_factor_authentication.md
@@ -1 +1,5 @@
+---
+redirect_to: '../user/profile/account/two_factor_authentication.md'
+---
+
This document was moved to [user/profile/account](../user/profile/account/two_factor_authentication.md).
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index dcd5e6e2245..c78cfc41678 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -100,6 +100,13 @@ the gitlab task runner pod via `kubectl`. Refer to [backing up a GitLab installa
kubectl exec -it <gitlab task-runner pod> backup-utility
```
+Similarly to the Kubernetes case, if you have scaled out your GitLab
+cluster to use multiple application servers, you should pick a
+designated node (that won't be auto-scaled away) for running the
+backup rake task. Because the backup rake task is tightly coupled to
+the main Rails application, this is typically a node on which you're
+also running Unicorn/Puma and/or Sidekiq.
+
Example output:
```
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index eef26612d5b..0fc7f57feab 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -21,7 +21,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa)
- **Articles:**
- [How to Configure LDAP with GitLab CE](../../administration/auth/how_to_configure_ldap_gitlab_ce/index.md)
- - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.md)
+ - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html)
- [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/)
- [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/support-engineering/ldap/debugging_ldap.html)
- **Integrations:**
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 195dd3e8846..c73e810545e 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -442,10 +442,10 @@ process. Auto Review Apps create a Review App for each branch.
Auto Review Apps will deploy your app to your Kubernetes cluster only. When no cluster
is available, no deployment will occur.
-The Review App will have a unique URL based on the project name, the branch
+The Review App will have a unique URL based on the project ID, the branch or tag
name, and a unique number, combined with the Auto DevOps base domain. For
-example, `user-project-branch-1234.example.com`. A link to the Review App shows
-up in the merge request widget for easy discovery. When the branch is deleted,
+example, `13083-review-project-branch-123456.example.com`. A link to the Review App shows
+up in the merge request widget for easy discovery. When the branch or tag is deleted,
for example after the merge request is merged, the Review App will automatically
be deleted.
@@ -506,17 +506,22 @@ enable them.
You can make use of [environment variables](#environment-variables) to automatically
scale your pod replicas.
-It's important to note that when a project is deployed to a Kubernetes cluster,
-it relies on a Docker image that has been pushed to the
-[GitLab Container Registry](../../user/project/container_registry.md). Kubernetes
-fetches this image and uses it to run the application. If the project is public,
-the image can be accessed by Kubernetes without any authentication, allowing us
-to have deployments more usable. If the project is private/internal, the
-Registry requires credentials to pull the image. Currently, this is addressed
-by providing `CI_JOB_TOKEN` as the password that can be used, but this token will
-no longer be valid as soon as the deployment job finishes. This means that
-Kubernetes can run the application, but in case it should be restarted or
-executed somewhere else, it cannot be accessed again.
+> [Introduced][ce-19507] in GitLab 11.0.
+
+For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md#gitlab-deploy-token)
+will be automatically created, when Auto DevOps is enabled and the Auto DevOps settings are saved. This Deploy Token
+can be used for permanent access to the registry.
+
+If the GitLab Deploy Token cannot be found, `CI_REGISTRY_PASSWORD` is
+used. Note that `CI_REGISTRY_PASSWORD` is only valid during deployment.
+This means that Kubernetes will be able to successfully pull the
+container image during deployment but in cases where the image needs to
+be pulled again, e.g. after pod eviction, Kubernetes will fail to do so
+as it will be attempting to fetch the image using
+`CI_REGISTRY_PASSWORD`.
+
+NOTE: **Note:**
+When the GitLab Deploy Token has been manually revoked, it won't be automatically created.
#### Migrations
@@ -551,15 +556,6 @@ The `/app` path is the directory of your project inside the docker image
as [configured by
Herokuish](https://github.com/gliderlabs/herokuish#paths)
-> [Introduced][ce-19507] in GitLab 11.0.
-
-For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md#gitlab-deploy-token)
-will be automatically created, when Auto DevOps is enabled and the Auto DevOps settings are saved. This Deploy Token
-can be used for permanent access to the registry.
-
-Note: **Note**
-When the GitLab Deploy Token has been manually revoked, it won't be automatically created.
-
### Auto Monitoring
NOTE: **Note:**
@@ -708,6 +704,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `INCREMENTAL_ROLLOUT_MODE`| From GitLab 11.4, this variable, if present, can be used to enable an [incremental rollout](#incremental-rollout-to-production-premium) of your application for the production environment.<br/>Set to: <ul><li>`manual`, for manual deployment jobs.</li><li>`timed`, for automatic rollout deployments with a 5 minute delay each one.</li></ul> |
| `TEST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `test` job. If the variable is present, the job will not be created. |
| `CODE_QUALITY_DISABLED` | From GitLab 11.0, this variable can be used to disable the `codequality` job. If the variable is present, the job will not be created. |
+| `LICENSE_MANAGEMENT_DISABLED` | From GitLab 11.0, this variable can be used to disable the `license_management` job. If the variable is present, the job will not be created. |
| `SAST_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast` job. If the variable is present, the job will not be created. |
| `DEPENDENCY_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `dependency_scanning` job. If the variable is present, the job will not be created. |
| `CONTAINER_SCANNING_DISABLED` | From GitLab 11.0, this variable can be used to disable the `sast:container` job. If the variable is present, the job will not be created. |
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 01979f12a01..a1825581ebf 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -26,7 +26,7 @@ If you want to disable it for a specific project, you can do so in
The maximum size of the [job artifacts][art-yml] can be set in the Admin area
of your GitLab instance. The value is in *MB* and the default is 100MB per job;
-on GitLab.com it's [set to 1G](../../gitlab_com/index.md#gitlab-ci-cd).
+on GitLab.com it's [set to 1G](../../gitlab_com/index.md#gitlab-cicd).
To change it:
@@ -40,7 +40,7 @@ The default expiration time of the [job artifacts](../../../administration/job_a
can be set in the Admin area of your GitLab instance. The syntax of duration is
described in [`artifacts:expire_in`](../../../ci/yaml/README.md#artifactsexpire_in)
and the default value is `30 days`. On GitLab.com they
-[never expire](../../gitlab_com/index.md#gitlab-ci-cd).
+[never expire](../../gitlab_com/index.md#gitlab-cicd).
1. Go to **Admin area > Settings > Continuous Integration and Deployment**.
1. Change the value of default expiration time.
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 762cf911fcf..5dc798fd8d3 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -51,7 +51,7 @@ Below are the settings for [GitLab Pages].
| TLS certificates support| yes | no |
The maximum size of your Pages site is regulated by the artifacts maximum size
-which is part of [GitLab CI/CD](#gitlab-ci-cd).
+which is part of [GitLab CI/CD](#gitlab-cicd).
## GitLab CI/CD
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
index 8cdfb13a97b..6d67688fdff 100644
--- a/doc/user/group/clusters/index.md
+++ b/doc/user/group/clusters/index.md
@@ -72,11 +72,10 @@ The domain should have a wildcard DNS configured to the Ingress IP address.
## Environment scopes **[PREMIUM]**
-When adding more than one Kubernetes cluster to your project, you need
-to differentiate them with an environment scope. The environment scope
-associates clusters with [environments](../../../ci/environments.md)
-similar to how the [environment-specific
-variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables)
+When adding more than one Kubernetes cluster to your project, you need to differentiate
+them with an environment scope. The environment scope associates clusters with
+[environments](../../../ci/environments.md) similar to how the
+[environment-specific variables](https://docs.gitlab.com/ee/ci/variables/README.html#limiting-environment-scopes-of-variables-premium)
work.
While evaluating which environment matches the environment scope of a
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index c1f50bcc593..1fe8017adbc 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -168,20 +168,21 @@ Alternatively, you can [lock the sharing with group feature](#share-with-group-l
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups.
See [the GitLab Enterprise Edition documentation](../../integration/ldap.md) for more information.
-## Transfer groups to another group
+## Transferring groups
-From 10.5 there are two different ways to transfer a group:
+From GitLab 10.5, groups can be transferred in the following ways:
-- Either by transferring a group into another group (making it a subgroup of that group).
-- Or by converting a subgroup into a root group (a group with no parent).
+- Top-level groups can be transferred to a group, converting them into subgroups.
+- Subgroups can be transferred to a new parent group.
+- Subgroups can be transferred out from a parent group, converting them into top-level groups.
-Please make sure to understand that:
+When transferring groups, note:
-- Changing a group's parent can have unintended side effects. See [Redirects when changing repository paths](https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths)
-- You can only transfer the group to a group you manage.
+- Changing a group's parent can have unintended side effects. See [Redirects when changing repository paths](../project/index.md#redirects-when-changing-repository-paths).
+- You can only transfer groups to groups you manage.
- You will need to update your local repositories to point to the new location.
-- If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
-- Only explicit group membership is transferred, not the inherited membership. If this would leave the group without an owner, the transferring user is added as owner instead.
+- If the parent group's visibility is lower than the group's current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
+- Only explicit group membership is transferred, not inherited membership. If the group's owners have only inherited membership, this would leave the group without an owner. In this case, the user transferring the group becomes the group's owner.
## Group settings
diff --git a/doc/user/instance_statistics/convdev.md b/doc/user/instance_statistics/convdev.md
index 247be1fb392..2c9e0ecbf65 100644
--- a/doc/user/instance_statistics/convdev.md
+++ b/doc/user/instance_statistics/convdev.md
@@ -2,7 +2,7 @@
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30469) in GitLab 9.3.
-NOTE: **NOTE**
+NOTE: **Note:**
Your GitLab instance's [usage ping](../admin_area/settings/usage_statistics.md#usage-ping-core-only) must be activated in order to use this feature.
The Conversational Development Index (ConvDev Index) gives you an overview of your entire
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 833b9b66102..9db1e43fdf2 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -23,6 +23,12 @@ To add or import a user, you can follow the
See our [product handbook on permissions](https://about.gitlab.com/handbook/product#permissions-in-gitlab)
+## Instance-wide user permissions
+
+By default, users can create top-level groups and change their
+usernames. A GitLab administrator can configure the GitLab instance to
+[modify this behavior](../administration/user_settings.md).
+
## Project members permissions
NOTE: **Note:**
diff --git a/doc/user/profile/img/personal_access_tokens.png b/doc/user/profile/img/personal_access_tokens.png
deleted file mode 100644
index d29f4cb0a20..00000000000
--- a/doc/user/profile/img/personal_access_tokens.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index 7d55048c994..3a4d09c35d9 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -20,21 +20,19 @@ at midnight UTC.
You can create as many personal access tokens as you like from your GitLab
profile.
-1. Log in to your GitLab account.
-1. Go to your **Profile settings**.
-1. Go to **Access tokens**.
-1. Choose a name and optionally an expiry date for the token.
+1. Log in to GitLab.
+1. In the upper-right corner, click your avatar and select **Settings**.
+1. On the **User Settings** menu, select **Access Tokens**.
+1. Choose a name and optional expiry date for the token.
1. Choose the [desired scopes](#limiting-scopes-of-a-personal-access-token).
-1. Click on **Create personal access token**.
+1. Click the **Create personal access token** button.
1. Save the personal access token somewhere safe. Once you leave or refresh
the page, you won't be able to access it again.
-![Personal access tokens page](img/personal_access_tokens.png)
+### Revoking a personal access token
-## Revoking a personal access token
-
-At any time, you can revoke any personal access token by just clicking the
-respective **Revoke** button under the 'Active personal access tokens' area.
+At any time, you can revoke any personal access token by clicking the
+respective **Revoke** button under the **Active Personal Access Token** area.
## Limiting scopes of a personal access token
diff --git a/doc/user/project/badges.md b/doc/user/project/badges.md
index 19eb95099ce..025f3af0af5 100644
--- a/doc/user/project/badges.md
+++ b/doc/user/project/badges.md
@@ -63,6 +63,12 @@ are available:
- `%{commit_sha}`: ID of the most recent commit to the default branch of a
project's repository
+NOTE: **NOTE**
+Placeholders allow badges to expose otherwise-private information, such as the
+default branch or commit SHA when the project is configured to have a private
+repository. This is by design, as badges are intended to be used publicly. Avoid
+using these placeholders if the information is sensitive.
+
## API
You can also configure badges via the GitLab API. As in the settings, there is
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index ef85b2f6837..d1206b0c80a 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -75,6 +75,14 @@ new Kubernetes cluster to your project:
After a couple of minutes, your cluster will be ready to go. You can now proceed
to install some [pre-defined applications](#installing-applications).
+NOTE: **Note:**
+GitLab requires basic authentication enabled and a client certificate issued for
+the cluster in order to setup an [initial service
+account](#access-controls). Starting from [GitLab
+11.10](https://gitlab.com/gitlab-org/gitlab-ce/issues/58208), the cluster
+creation process will explicitly request that basic authentication and
+client certificate is enabled.
+
## Adding an existing Kubernetes cluster
To add an existing Kubernetes cluster to your project:
@@ -477,12 +485,9 @@ differentiate the new cluster with the rest.
## Setting the environment scope **[PREMIUM]**
-When adding more than one Kubernetes clusters to your project, you need
-to differentiate them with an environment scope. The environment scope
-associates clusters with [environments](../../../ci/environments.md)
-similar to how the [environment-specific
-variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables)
-work.
+When adding more than one Kubernetes cluster to your project, you need to differentiate
+them with an environment scope. The environment scope associates clusters with [environments](../../../ci/environments.md) similar to how the
+[environment-specific variables](https://docs.gitlab.com/ee/ci/variables/README.html#limiting-environment-scopes-of-variables-premium) work.
The default environment scope is `*`, which means all jobs, regardless of their
environment, will use that cluster. Each scope can only be used by a single
@@ -545,7 +550,7 @@ GitLab CI/CD build environment.
| `KUBE_CA_PEM_FILE` | Path to a file containing PEM data. Only present if a custom CA bundle was specified. |
| `KUBE_CA_PEM` | (**deprecated**) Raw PEM data. Only if a custom CA bundle was specified. |
| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. This config also embeds the same token defined in `KUBE_TOKEN` so you likely will only need this variable. This variable name is also automatically picked up by `kubectl` so you won't actually need to reference it explicitly if using `kubectl`. |
-| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](#base-domain) for more information. | 
+| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](#base-domain) for more information. |
NOTE: **NOTE:**
Prior to GitLab 11.5, `KUBE_TOKEN` was the Kubernetes token of the main
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 26989e2a8a4..43a9e24526d 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -13,7 +13,7 @@ There are two ways to set up Prometheus integration, depending on where your app
- For deployments on Kubernetes, GitLab can automatically [deploy and manage Prometheus](#managed-prometheus-on-kubernetes).
- For other deployment targets, simply [specify the Prometheus server](#manual-configuration-of-prometheus).
-Once enabled, GitLab will automatically detect metrics from known services in the [metric library](#monitoring-ci-cd-environments).
+Once enabled, GitLab will automatically detect metrics from known services in the [metric library](#monitoring-cicd-environments).
## Enabling Prometheus Integration
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index c3fc6d4b859..d324e0de0cc 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -1041,7 +1041,12 @@ X-Gitlab-Event: Pipeline Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
- "runner": null,
+ "runner": {
+ "id":380987,
+ "description":"shared-runners-manager-6.gitlab.com",
+ "active":true,
+ "is_shared":true
+ },
"artifacts_file":{
"filename": null,
"size": null
@@ -1062,7 +1067,12 @@ X-Gitlab-Event: Pipeline Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
- "runner": null,
+ "runner": {
+ "id":380987,
+ "description":"shared-runners-manager-6.gitlab.com",
+ "active":true,
+ "is_shared":true
+ },
"artifacts_file":{
"filename": null,
"size": null
@@ -1083,7 +1093,12 @@ X-Gitlab-Event: Pipeline Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
},
- "runner": null,
+ "runner": {
+ "id":380987,
+ "description":"shared-runners-manager-6.gitlab.com",
+ "active":true,
+ "is_shared":true
+ },
"artifacts_file":{
"filename": null,
"size": null
diff --git a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
index bdd7d0022e6..f8af71ab46b 100644
--- a/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
+++ b/doc/user/project/merge_requests/merge_when_pipeline_succeeds.md
@@ -1,4 +1,4 @@
-# Merge When Pipeline Succeeds
+# Merge when pipeline succeeds
When reviewing a merge request that looks ready to merge but still has one or
more CI jobs running, you can set it to be merged automatically when the
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index d7a1a69f29d..d41b65f7985 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -49,7 +49,7 @@ It is important to note that we have a few types of users:
Administrator will have to be a member of it in order to have access to it
via another project's job.
-- **External users**: CI jobs created by [external users][ext] will have
+- **External users**: CI jobs created by [external users](../permissions.md#external-users-permissions) will have
access only to projects to which user has at least reporter access. This
rules out accessing all internal projects by default,
@@ -60,7 +60,7 @@ Let's consider the following scenario:
hosted in private repositories and you have multiple CI jobs that make use
of these repositories.
-1. You invite a new [external user][ext]. CI jobs created by that user do not
+1. You invite a new [external user](../permissions.md#external-users-permissions). CI jobs created by that user do not
have access to internal repositories, because the user also doesn't have the
access from within GitLab. You as an employee have to grant explicit access
for this user. This allows us to prevent from accidental data leakage.
@@ -232,7 +232,6 @@ test:
[job permissions]: ../permissions.md#job-permissions
[comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302
-[ext]: ../permissions.md#external-users
[gitsub]: ../../ci/git_submodules.md
[https]: ../admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols
[triggers]: ../../ci/triggers/README.md
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index 6bb58689f38..f67ef1e6a46 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -115,7 +115,7 @@ gives you absolute control over the build process. You can actually watch your
website being built live by following the CI job traces.
For a simplified user guide on setting up GitLab CI/CD for Pages, read through
-the article [GitLab Pages from A to Z: Part 4 - Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md#creating-and-tweaking-gitlab-ci-yml-for-gitlab-pages)
+the article [GitLab Pages from A to Z: Part 4 - Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
> **Note:**
> Before reading this section, make sure you familiarize yourself with GitLab CI
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index 8b57129c9e1..5271c76fc24 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -183,7 +183,7 @@ information in the UI.
DANGER: **Warning:**
This is a destructive action that leads to data loss. Use with caution.
-If you have at least Developer [permissions](../../permissions.md#gitlab-ci-cd-permissions)
+If you have at least Developer [permissions](../../permissions.md#gitlab-cicd-permissions)
on the project, you can erase a single job via the UI which will also remove the
artifacts and the job's trace.
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 3eb8123144f..480cc921d76 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -96,7 +96,7 @@ all matching branches:
## Creating a protected branch
-> [Introduced][https://gitlab.com/gitlab-org/gitlab-ce/issues/53361] in GitLab 11.9.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53361) in GitLab 11.9.
When a protected branch or wildcard protected branches are set to
[**No one** is **Allowed to push**](#using-the-allowed-to-merge-and-allowed-to-push-settings),
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 0ebe5eea173..fffb6a5d86d 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -1 +1,5 @@
+---
+redirect_to: '../user/project/integrations/webhooks.md'
+---
+
This document was moved to [project/integrations/webhooks](../user/project/integrations/webhooks.md).
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 91eb6a23701..8afe6dda414 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -7,9 +7,7 @@ module API
before { authenticate! }
- NOTEABLE_TYPES = [Issue, Snippet, MergeRequest, Commit].freeze
-
- NOTEABLE_TYPES.each do |noteable_type|
+ Helpers::DiscussionsHelpers.noteable_types.each do |noteable_type|
parent_type = noteable_type.parent_class.to_s.underscore
noteables_str = noteable_type.to_s.underscore.pluralize
noteables_path = noteable_type == Commit ? "repository/#{noteables_str}" : noteables_str
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 64958ff982a..cb0d6d96f29 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -58,6 +58,22 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ def create_group
+ # This is a separate method so that EE can extend its behaviour, without
+ # having to modify this code directly.
+ ::Groups::CreateService
+ .new(current_user, declared_params(include_missing: false))
+ .execute
+ end
+
+ def update_group(group)
+ # This is a separate method so that EE can extend its behaviour, without
+ # having to modify this code directly.
+ ::Groups::UpdateService
+ .new(group, current_user, declared_params(include_missing: false))
+ .execute
+ end
+
def find_group_projects(params)
group = find_group!(params[:id])
options = {
@@ -127,7 +143,7 @@ module API
authorize! :create_group
end
- group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
+ group = create_group
if group.persisted?
present group, with: Entities::GroupDetail, current_user: current_user
@@ -153,7 +169,7 @@ module API
group = find_group!(params[:id])
authorize! :admin_group, group
- if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
+ if update_group(group)
present group, with: Entities::GroupDetail, current_user: current_user
else
render_validation_error!(group)
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 825fab62034..b8bd180bdc1 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -6,6 +6,7 @@ module API
include Helpers::Pagination
SUDO_HEADER = "HTTP_SUDO".freeze
+ GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret".freeze
SUDO_PARAM = :sudo
API_USER_ENV = 'gitlab.api.user'.freeze
@@ -212,10 +213,12 @@ module API
end
def authenticate_by_gitlab_shell_token!
- input = params['secret_token'].try(:chomp)
- unless Devise.secure_compare(secret_token, input)
- unauthorized!
- end
+ input = params['secret_token']
+ input ||= Base64.decode64(headers[GITLAB_SHARED_SECRET_HEADER]) if headers.key?(GITLAB_SHARED_SECRET_HEADER)
+
+ input&.chomp!
+
+ unauthorized! unless Devise.secure_compare(secret_token, input)
end
def authenticated_with_full_private_access!
diff --git a/lib/api/helpers/discussions_helpers.rb b/lib/api/helpers/discussions_helpers.rb
new file mode 100644
index 00000000000..94a5bf75c39
--- /dev/null
+++ b/lib/api/helpers/discussions_helpers.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module DiscussionsHelpers
+ def self.noteable_types
+ # This is a method instead of a constant, allowing EE to more easily
+ # extend it.
+ [Issue, Snippet, MergeRequest, Commit]
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb
index 795dca5cf03..a068de4361c 100644
--- a/lib/api/helpers/notes_helpers.rb
+++ b/lib/api/helpers/notes_helpers.rb
@@ -3,6 +3,12 @@
module API
module Helpers
module NotesHelpers
+ def self.noteable_types
+ # This is a method instead of a constant, allowing EE to more easily
+ # extend it.
+ [Issue, MergeRequest, Snippet]
+ end
+
def update_note(noteable, note_id)
note = noteable.notes.find(params[:note_id])
diff --git a/lib/api/helpers/resource_label_events_helpers.rb b/lib/api/helpers/resource_label_events_helpers.rb
new file mode 100644
index 00000000000..23574deb59b
--- /dev/null
+++ b/lib/api/helpers/resource_label_events_helpers.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module ResourceLabelEventsHelpers
+ def self.eventable_types
+ # This is a method instead of a constant, allowing EE to more easily
+ # extend it.
+ [Issue, MergeRequest]
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/search_helpers.rb b/lib/api/helpers/search_helpers.rb
new file mode 100644
index 00000000000..47fb5a36327
--- /dev/null
+++ b/lib/api/helpers/search_helpers.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module SearchHelpers
+ def self.global_search_scopes
+ # This is a separate method so that EE can redefine it.
+ %w(projects issues merge_requests milestones snippet_titles snippet_blobs)
+ end
+
+ def self.group_search_scopes
+ # This is a separate method so that EE can redefine it.
+ %w(projects issues merge_requests milestones)
+ end
+
+ def self.project_search_scopes
+ # This is a separate method so that EE can redefine it.
+ %w(issues merge_requests milestones notes wiki_blobs commits blobs)
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
new file mode 100644
index 00000000000..8582c45798f
--- /dev/null
+++ b/lib/api/helpers/services_helpers.rb
@@ -0,0 +1,721 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ # Helpers module for API::Services
+ #
+ # The data structures inside this model are returned using class methods,
+ # allowing EE to extend them where necessary.
+ module ServicesHelpers
+ def self.chat_notification_settings
+ [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The chat webhook'
+ },
+ {
+ required: false,
+ name: :username,
+ type: String,
+ desc: 'The chat username'
+ },
+ {
+ required: false,
+ name: :channel,
+ type: String,
+ desc: 'The default chat channel'
+ }
+ ].freeze
+ end
+
+ def self.chat_notification_flags
+ [
+ {
+ required: false,
+ name: :notify_only_broken_pipelines,
+ type: Boolean,
+ desc: 'Send notifications for broken pipelines'
+ },
+ {
+ required: false,
+ name: :notify_only_default_branch,
+ type: Boolean,
+ desc: 'Send notifications only for the default branch'
+ }
+ ].freeze
+ end
+
+ def self.chat_notification_channels
+ [
+ {
+ required: false,
+ name: :push_channel,
+ type: String,
+ desc: 'The name of the channel to receive push_events notifications'
+ },
+ {
+ required: false,
+ name: :issue_channel,
+ type: String,
+ desc: 'The name of the channel to receive issues_events notifications'
+ },
+ {
+ required: false,
+ name: :confidential_issue_channel,
+ type: String,
+ desc: 'The name of the channel to receive confidential_issues_events notifications'
+ },
+ {
+ required: false,
+ name: :merge_request_channel,
+ type: String,
+ desc: 'The name of the channel to receive merge_requests_events notifications'
+ },
+ {
+ required: false,
+ name: :note_channel,
+ type: String,
+ desc: 'The name of the channel to receive note_events notifications'
+ },
+ {
+ required: false,
+ name: :tag_push_channel,
+ type: String,
+ desc: 'The name of the channel to receive tag_push_events notifications'
+ },
+ {
+ required: false,
+ name: :pipeline_channel,
+ type: String,
+ desc: 'The name of the channel to receive pipeline_events notifications'
+ },
+ {
+ required: false,
+ name: :wiki_page_channel,
+ type: String,
+ desc: 'The name of the channel to receive wiki_page_events notifications'
+ }
+ ].freeze
+ end
+
+ def self.chat_notification_events
+ [
+ {
+ required: false,
+ name: :push_events,
+ type: Boolean,
+ desc: 'Enable notifications for push_events'
+ },
+ {
+ required: false,
+ name: :issues_events,
+ type: Boolean,
+ desc: 'Enable notifications for issues_events'
+ },
+ {
+ required: false,
+ name: :confidential_issues_events,
+ type: Boolean,
+ desc: 'Enable notifications for confidential_issues_events'
+ },
+ {
+ required: false,
+ name: :merge_requests_events,
+ type: Boolean,
+ desc: 'Enable notifications for merge_requests_events'
+ },
+ {
+ required: false,
+ name: :note_events,
+ type: Boolean,
+ desc: 'Enable notifications for note_events'
+ },
+ {
+ required: false,
+ name: :tag_push_events,
+ type: Boolean,
+ desc: 'Enable notifications for tag_push_events'
+ },
+ {
+ required: false,
+ name: :pipeline_events,
+ type: Boolean,
+ desc: 'Enable notifications for pipeline_events'
+ },
+ {
+ required: false,
+ name: :wiki_page_events,
+ type: Boolean,
+ desc: 'Enable notifications for wiki_page_events'
+ }
+ ].freeze
+ end
+
+ def self.services
+ {
+ 'asana' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'User API token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
+ }
+ ],
+ 'assembla' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The authentication token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Subdomain setting'
+ }
+ ],
+ 'bamboo' => [
+ {
+ required: true,
+ name: :bamboo_url,
+ type: String,
+ desc: 'Bamboo root URL like https://bamboo.example.com'
+ },
+ {
+ required: true,
+ name: :build_key,
+ type: String,
+ desc: 'Bamboo build plan key like'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with API access, if applicable'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'Password of the user'
+ }
+ ],
+ 'bugzilla' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'buildkite' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Buildkite project GitLab token'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The buildkite project URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'campfire' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Campfire token'
+ },
+ {
+ required: false,
+ name: :subdomain,
+ type: String,
+ desc: 'Campfire subdomain'
+ },
+ {
+ required: false,
+ name: :room,
+ type: String,
+ desc: 'Campfire room'
+ }
+ ],
+ 'custom-issue-tracker' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'New issue URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'Issues URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'Project URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'Description'
+ },
+ {
+ required: false,
+ name: :title,
+ type: String,
+ desc: 'Title'
+ }
+ ],
+ 'discord' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'Discord webhook. e.g. https://discordapp.com/api/webhooks/…'
+ }
+ ],
+ 'drone-ci' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Drone CI token'
+ },
+ {
+ required: true,
+ name: :drone_url,
+ type: String,
+ desc: 'Drone CI URL'
+ },
+ {
+ required: false,
+ name: :enable_ssl_verification,
+ type: Boolean,
+ desc: 'Enable SSL verification for communication'
+ }
+ ],
+ 'emails-on-push' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :disable_diffs,
+ type: Boolean,
+ desc: 'Disable code diffs'
+ },
+ {
+ required: false,
+ name: :send_from_committer_email,
+ type: Boolean,
+ desc: 'Send from committer'
+ }
+ ],
+ 'external-wiki' => [
+ {
+ required: true,
+ name: :external_wiki_url,
+ type: String,
+ desc: 'The URL of the external Wiki'
+ }
+ ],
+ 'flowdock' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'Flowdock token'
+ }
+ ],
+ 'hangouts-chat' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Hangouts Chat webhook. e.g. https://chat.googleapis.com/v1/spaces…'
+ },
+ chat_notification_events
+ ].flatten,
+ 'irker' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Recipients/channels separated by whitespaces'
+ },
+ {
+ required: false,
+ name: :default_irc_uri,
+ type: String,
+ desc: 'Default: irc://irc.network.net:6697'
+ },
+ {
+ required: false,
+ name: :server_host,
+ type: String,
+ desc: 'Server host. Default localhost'
+ },
+ {
+ required: false,
+ name: :server_port,
+ type: Integer,
+ desc: 'Server port. Default 6659'
+ },
+ {
+ required: false,
+ name: :colorize_messages,
+ type: Boolean,
+ desc: 'Colorize messages'
+ }
+ ],
+ 'jira' => [
+ {
+ required: true,
+ name: :url,
+ type: String,
+ desc: 'The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., https://jira.example.com'
+ },
+ {
+ required: false,
+ name: :api_url,
+ type: String,
+ desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'The username of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'The password of the user created to be used with GitLab/JIRA'
+ },
+ {
+ required: false,
+ name: :jira_issue_transition_id,
+ type: String,
+ desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
+ }
+ ],
+ 'kubernetes' => [
+ {
+ required: true,
+ name: :namespace,
+ type: String,
+ desc: 'The Kubernetes namespace to use'
+ },
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The service token to authenticate against the Kubernetes cluster with'
+ },
+ {
+ required: false,
+ name: :ca_pem,
+ type: String,
+ desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
+ }
+ ],
+ 'mattermost-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Mattermost token'
+ }
+ ],
+ 'slack-slash-commands' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Slack token'
+ }
+ ],
+ 'packagist' => [
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'The username'
+ },
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Packagist API token'
+ },
+ {
+ required: false,
+ name: :server,
+ type: String,
+ desc: 'The server'
+ }
+ ],
+ 'pipelines-email' => [
+ {
+ required: true,
+ name: :recipients,
+ type: String,
+ desc: 'Comma-separated list of recipient email addresses'
+ },
+ {
+ required: false,
+ name: :notify_only_broken_pipelines,
+ type: Boolean,
+ desc: 'Notify only broken pipelines'
+ }
+ ],
+ 'pivotaltracker' => [
+ {
+ required: true,
+ name: :token,
+ type: String,
+ desc: 'The Pivotaltracker token'
+ },
+ {
+ required: false,
+ name: :restrict_to_branch,
+ type: String,
+ desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
+ }
+ ],
+ 'prometheus' => [
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'Prometheus API Base URL, like http://prometheus.example.com/'
+ }
+ ],
+ 'pushover' => [
+ {
+ required: true,
+ name: :api_key,
+ type: String,
+ desc: 'The application key'
+ },
+ {
+ required: true,
+ name: :user_key,
+ type: String,
+ desc: 'The user key'
+ },
+ {
+ required: true,
+ name: :priority,
+ type: String,
+ desc: 'The priority'
+ },
+ {
+ required: true,
+ name: :device,
+ type: String,
+ desc: 'Leave blank for all active devices'
+ },
+ {
+ required: true,
+ name: :sound,
+ type: String,
+ desc: 'The sound of the notification'
+ }
+ ],
+ 'redmine' => [
+ {
+ required: true,
+ name: :new_issue_url,
+ type: String,
+ desc: 'The new issue URL'
+ },
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The project URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'The issues URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'The description of the tracker'
+ }
+ ],
+ 'youtrack' => [
+ {
+ required: true,
+ name: :project_url,
+ type: String,
+ desc: 'The project URL'
+ },
+ {
+ required: true,
+ name: :issues_url,
+ type: String,
+ desc: 'The issues URL'
+ },
+ {
+ required: false,
+ name: :description,
+ type: String,
+ desc: 'The description of the tracker'
+ }
+ ],
+ 'slack' => [
+ chat_notification_settings,
+ chat_notification_flags,
+ chat_notification_channels,
+ chat_notification_events
+ ].flatten,
+ 'microsoft-teams' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…'
+ }
+ ],
+ 'mattermost' => [
+ chat_notification_settings,
+ chat_notification_flags,
+ chat_notification_channels,
+ chat_notification_events
+ ].flatten,
+ 'teamcity' => [
+ {
+ required: true,
+ name: :teamcity_url,
+ type: String,
+ desc: 'TeamCity root URL like https://teamcity.example.com'
+ },
+ {
+ required: true,
+ name: :build_type,
+ type: String,
+ desc: 'Build configuration ID'
+ },
+ {
+ required: true,
+ name: :username,
+ type: String,
+ desc: 'A user with permissions to trigger a manual build'
+ },
+ {
+ required: true,
+ name: :password,
+ type: String,
+ desc: 'The password of the user'
+ }
+ ]
+ }
+ end
+
+ def self.service_classes
+ [
+ ::AsanaService,
+ ::AssemblaService,
+ ::BambooService,
+ ::BugzillaService,
+ ::BuildkiteService,
+ ::CampfireService,
+ ::CustomIssueTrackerService,
+ ::DiscordService,
+ ::DroneCiService,
+ ::EmailsOnPushService,
+ ::ExternalWikiService,
+ ::FlowdockService,
+ ::HangoutsChatService,
+ ::IrkerService,
+ ::JiraService,
+ ::KubernetesService,
+ ::MattermostSlashCommandsService,
+ ::SlackSlashCommandsService,
+ ::PackagistService,
+ ::PipelinesEmailService,
+ ::PivotaltrackerService,
+ ::PrometheusService,
+ ::PushoverService,
+ ::RedmineService,
+ ::YoutrackService,
+ ::SlackService,
+ ::MattermostService,
+ ::MicrosoftTeamsService,
+ ::TeamcityService
+ ]
+ end
+
+ def self.development_service_classes
+ [
+ ::MockCiService,
+ ::MockDeploymentService,
+ ::MockMonitoringService
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 70b32f7d758..7f4a00f1389 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -15,6 +15,12 @@ module API
status code
{ status: success, message: message }.merge(extra_options).compact
end
+
+ def lfs_authentication_url(project)
+ # This is a separate method so that EE can alter its behaviour more
+ # easily.
+ project.http_url_to_repo
+ end
end
namespace 'internal' do
@@ -81,11 +87,6 @@ module API
gl_id: Gitlab::GlId.gl_id(user),
gl_username: user&.username,
git_config_options: [],
-
- # This repository_path is a bogus value but gitlab-shell still requires
- # its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135
- repository_path: '/',
-
gitaly: gitaly_payload(params[:action])
}
@@ -118,7 +119,9 @@ module API
raise ActiveRecord::RecordNotFound.new("No key_id or user_id passed!")
end
- Gitlab::LfsToken.new(actor).authentication_payload(project.http_url_to_repo)
+ Gitlab::LfsToken
+ .new(actor)
+ .authentication_payload(lfs_authentication_url(project))
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index f7bd092ce50..416cf39d3ec 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -7,9 +7,7 @@ module API
before { authenticate! }
- NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
-
- NOTEABLE_TYPES.each do |noteable_type|
+ Helpers::NotesHelpers.noteable_types.each do |noteable_type|
parent_type = noteable_type.parent_class.to_s.underscore
noteables_str = noteable_type.to_s.underscore.pluralize
diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb
index ca24742b7a3..9ecbf37b49a 100644
--- a/lib/api/project_milestones.rb
+++ b/lib/api/project_milestones.rb
@@ -103,17 +103,15 @@ module API
detail 'This feature was introduced in GitLab 11.9'
end
post ':id/milestones/:milestone_id/promote' do
- begin
- authorize! :admin_milestone, user_project
- authorize! :admin_milestone, user_project.group
+ authorize! :admin_milestone, user_project
+ authorize! :admin_milestone, user_project.group
- milestone = user_project.milestones.find(params[:milestone_id])
- Milestones::PromoteService.new(user_project, current_user).execute(milestone)
+ milestone = user_project.milestones.find(params[:milestone_id])
+ Milestones::PromoteService.new(user_project, current_user).execute(milestone)
- status(200)
- rescue Milestones::PromoteService::PromoteMilestoneError => error
- render_api_error!(error.message, 400)
- end
+ status(200)
+ rescue Milestones::PromoteService::PromoteMilestoneError => error
+ render_api_error!(error.message, 400)
end
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 32e05d84491..4106a2cdf38 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -89,11 +89,9 @@ module API
optional :format, type: String, desc: 'The archive format'
end
get ':id/repository/archive', requirements: { format: Gitlab::PathRegex.archive_formats_regex } do
- begin
- send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true
- rescue
- not_found!('File')
- end
+ send_git_archive user_project.repository, ref: params[:sha], format: params[:format], append_sha: true
+ rescue
+ not_found!('File')
end
desc 'Compare two branches, tags, or commits' do
@@ -118,12 +116,10 @@ module API
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
end
get ':id/repository/contributors' do
- begin
- contributors = ::Kaminari.paginate_array(user_project.repository.contributors(order_by: params[:order_by], sort: params[:sort]))
- present paginate(contributors), with: Entities::Contributor
- rescue
- not_found!
- end
+ contributors = ::Kaminari.paginate_array(user_project.repository.contributors(order_by: params[:order_by], sort: params[:sort]))
+ present paginate(contributors), with: Entities::Contributor
+ rescue
+ not_found!
end
desc 'Get the common ancestor between commits' do
diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb
index 0c328f7268e..448bef12cec 100644
--- a/lib/api/resource_label_events.rb
+++ b/lib/api/resource_label_events.rb
@@ -7,9 +7,7 @@ module API
before { authenticate! }
- EVENTABLE_TYPES = [Issue, MergeRequest].freeze
-
- EVENTABLE_TYPES.each do |eventable_type|
+ Helpers::ResourceLabelEventsHelpers.eventable_types.each do |eventable_type|
parent_type = eventable_type.parent_class.to_s.underscore
eventables_str = eventable_type.to_s.underscore.pluralize
diff --git a/lib/api/search.rb b/lib/api/search.rb
index f5db692afe5..f65e810bf90 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -45,6 +45,12 @@ module API
def entity
SCOPE_ENTITY[params[:scope].to_sym]
end
+
+ def verify_search_scope!
+ # In EE we have additional validation requirements for searches.
+ # Defining this method here as a noop allows us to easily extend it in
+ # EE, without having to modify this file directly.
+ end
end
resource :search do
@@ -55,12 +61,13 @@ module API
requires :search, type: String, desc: 'The expression it should be searched for'
requires :scope,
type: String,
- desc: 'The scope of search, available scopes:
- projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs',
- values: %w(projects issues merge_requests milestones snippet_titles snippet_blobs)
+ desc: 'The scope of the search',
+ values: Helpers::SearchHelpers.global_search_scopes
use :pagination
end
get do
+ verify_search_scope!
+
present search, with: entity
end
end
@@ -74,12 +81,13 @@ module API
requires :search, type: String, desc: 'The expression it should be searched for'
requires :scope,
type: String,
- desc: 'The scope of search, available scopes:
- projects, issues, merge_requests, milestones',
- values: %w(projects issues merge_requests milestones)
+ desc: 'The scope of the search',
+ values: Helpers::SearchHelpers.group_search_scopes
use :pagination
end
get ':id/(-/)search' do
+ verify_search_scope!
+
present search(group_id: user_group.id), with: entity
end
end
@@ -93,9 +101,8 @@ module API
requires :search, type: String, desc: 'The expression it should be searched for'
requires :scope,
type: String,
- desc: 'The scope of search, available scopes:
- issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs',
- values: %w(issues merge_requests milestones notes wiki_blobs commits blobs)
+ desc: 'The scope of the search',
+ values: Helpers::SearchHelpers.project_search_scopes
use :pagination
end
get ':id/(-/)search' do
diff --git a/lib/api/services.rb b/lib/api/services.rb
index bda6be51553..bc77fae87fa 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -1,696 +1,8 @@
# frozen_string_literal: true
module API
class Services < Grape::API
- CHAT_NOTIFICATION_SETTINGS = [
- {
- required: true,
- name: :webhook,
- type: String,
- desc: 'The chat webhook'
- },
- {
- required: false,
- name: :username,
- type: String,
- desc: 'The chat username'
- },
- {
- required: false,
- name: :channel,
- type: String,
- desc: 'The default chat channel'
- }
- ].freeze
-
- CHAT_NOTIFICATION_FLAGS = [
- {
- required: false,
- name: :notify_only_broken_pipelines,
- type: Boolean,
- desc: 'Send notifications for broken pipelines'
- },
- {
- required: false,
- name: :notify_only_default_branch,
- type: Boolean,
- desc: 'Send notifications only for the default branch'
- }
- ].freeze
-
- CHAT_NOTIFICATION_CHANNELS = [
- {
- required: false,
- name: :push_channel,
- type: String,
- desc: 'The name of the channel to receive push_events notifications'
- },
- {
- required: false,
- name: :issue_channel,
- type: String,
- desc: 'The name of the channel to receive issues_events notifications'
- },
- {
- required: false,
- name: :confidential_issue_channel,
- type: String,
- desc: 'The name of the channel to receive confidential_issues_events notifications'
- },
- {
- required: false,
- name: :merge_request_channel,
- type: String,
- desc: 'The name of the channel to receive merge_requests_events notifications'
- },
- {
- required: false,
- name: :note_channel,
- type: String,
- desc: 'The name of the channel to receive note_events notifications'
- },
- {
- required: false,
- name: :tag_push_channel,
- type: String,
- desc: 'The name of the channel to receive tag_push_events notifications'
- },
- {
- required: false,
- name: :pipeline_channel,
- type: String,
- desc: 'The name of the channel to receive pipeline_events notifications'
- },
- {
- required: false,
- name: :wiki_page_channel,
- type: String,
- desc: 'The name of the channel to receive wiki_page_events notifications'
- }
- ].freeze
-
- CHAT_NOTIFICATION_EVENTS = [
- {
- required: false,
- name: :push_events,
- type: Boolean,
- desc: 'Enable notifications for push_events'
- },
- {
- required: false,
- name: :issues_events,
- type: Boolean,
- desc: 'Enable notifications for issues_events'
- },
- {
- required: false,
- name: :confidential_issues_events,
- type: Boolean,
- desc: 'Enable notifications for confidential_issues_events'
- },
- {
- required: false,
- name: :merge_requests_events,
- type: Boolean,
- desc: 'Enable notifications for merge_requests_events'
- },
- {
- required: false,
- name: :note_events,
- type: Boolean,
- desc: 'Enable notifications for note_events'
- },
- {
- required: false,
- name: :tag_push_events,
- type: Boolean,
- desc: 'Enable notifications for tag_push_events'
- },
- {
- required: false,
- name: :pipeline_events,
- type: Boolean,
- desc: 'Enable notifications for pipeline_events'
- },
- {
- required: false,
- name: :wiki_page_events,
- type: Boolean,
- desc: 'Enable notifications for wiki_page_events'
- }
- ].freeze
-
- services = {
- 'asana' => [
- {
- required: true,
- name: :api_key,
- type: String,
- desc: 'User API token'
- },
- {
- required: false,
- name: :restrict_to_branch,
- type: String,
- desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches'
- }
- ],
- 'assembla' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The authentication token'
- },
- {
- required: false,
- name: :subdomain,
- type: String,
- desc: 'Subdomain setting'
- }
- ],
- 'bamboo' => [
- {
- required: true,
- name: :bamboo_url,
- type: String,
- desc: 'Bamboo root URL like https://bamboo.example.com'
- },
- {
- required: true,
- name: :build_key,
- type: String,
- desc: 'Bamboo build plan key like'
- },
- {
- required: true,
- name: :username,
- type: String,
- desc: 'A user with API access, if applicable'
- },
- {
- required: true,
- name: :password,
- type: String,
- desc: 'Passord of the user'
- }
- ],
- 'bugzilla' => [
- {
- required: true,
- name: :new_issue_url,
- type: String,
- desc: 'New issue URL'
- },
- {
- required: true,
- name: :issues_url,
- type: String,
- desc: 'Issues URL'
- },
- {
- required: true,
- name: :project_url,
- type: String,
- desc: 'Project URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'Description'
- },
- {
- required: false,
- name: :title,
- type: String,
- desc: 'Title'
- }
- ],
- 'buildkite' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'Buildkite project GitLab token'
- },
- {
- required: true,
- name: :project_url,
- type: String,
- desc: 'The buildkite project URL'
- },
- {
- required: false,
- name: :enable_ssl_verification,
- type: Boolean,
- desc: 'Enable SSL verification for communication'
- }
- ],
- 'campfire' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'Campfire token'
- },
- {
- required: false,
- name: :subdomain,
- type: String,
- desc: 'Campfire subdomain'
- },
- {
- required: false,
- name: :room,
- type: String,
- desc: 'Campfire room'
- }
- ],
- 'custom-issue-tracker' => [
- {
- required: true,
- name: :new_issue_url,
- type: String,
- desc: 'New issue URL'
- },
- {
- required: true,
- name: :issues_url,
- type: String,
- desc: 'Issues URL'
- },
- {
- required: true,
- name: :project_url,
- type: String,
- desc: 'Project URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'Description'
- },
- {
- required: false,
- name: :title,
- type: String,
- desc: 'Title'
- }
- ],
- 'discord' => [
- {
- required: true,
- name: :webhook,
- type: String,
- desc: 'Discord webhook. e.g. https://discordapp.com/api/webhooks/…'
- }
- ],
- 'drone-ci' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'Drone CI token'
- },
- {
- required: true,
- name: :drone_url,
- type: String,
- desc: 'Drone CI URL'
- },
- {
- required: false,
- name: :enable_ssl_verification,
- type: Boolean,
- desc: 'Enable SSL verification for communication'
- }
- ],
- 'emails-on-push' => [
- {
- required: true,
- name: :recipients,
- type: String,
- desc: 'Comma-separated list of recipient email addresses'
- },
- {
- required: false,
- name: :disable_diffs,
- type: Boolean,
- desc: 'Disable code diffs'
- },
- {
- required: false,
- name: :send_from_committer_email,
- type: Boolean,
- desc: 'Send from committer'
- }
- ],
- 'external-wiki' => [
- {
- required: true,
- name: :external_wiki_url,
- type: String,
- desc: 'The URL of the external Wiki'
- }
- ],
- 'flowdock' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'Flowdock token'
- }
- ],
- 'hangouts-chat' => [
- {
- required: true,
- name: :webhook,
- type: String,
- desc: 'The Hangouts Chat webhook. e.g. https://chat.googleapis.com/v1/spaces…'
- },
- CHAT_NOTIFICATION_EVENTS
- ].flatten,
- 'irker' => [
- {
- required: true,
- name: :recipients,
- type: String,
- desc: 'Recipients/channels separated by whitespaces'
- },
- {
- required: false,
- name: :default_irc_uri,
- type: String,
- desc: 'Default: irc://irc.network.net:6697'
- },
- {
- required: false,
- name: :server_host,
- type: String,
- desc: 'Server host. Default localhost'
- },
- {
- required: false,
- name: :server_port,
- type: Integer,
- desc: 'Server port. Default 6659'
- },
- {
- required: false,
- name: :colorize_messages,
- type: Boolean,
- desc: 'Colorize messages'
- }
- ],
- 'jira' => [
- {
- required: true,
- name: :url,
- type: String,
- desc: 'The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., https://jira.example.com'
- },
- {
- required: false,
- name: :api_url,
- type: String,
- desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com'
- },
- {
- required: true,
- name: :username,
- type: String,
- desc: 'The username of the user created to be used with GitLab/JIRA'
- },
- {
- required: true,
- name: :password,
- type: String,
- desc: 'The password of the user created to be used with GitLab/JIRA'
- },
- {
- required: false,
- name: :jira_issue_transition_id,
- type: String,
- desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
- }
- ],
-
- 'kubernetes' => [
- {
- required: true,
- name: :namespace,
- type: String,
- desc: 'The Kubernetes namespace to use'
- },
- {
- required: true,
- name: :api_url,
- type: String,
- desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
- },
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The service token to authenticate against the Kubernetes cluster with'
- },
- {
- required: false,
- name: :ca_pem,
- type: String,
- desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
- }
- ],
- 'mattermost-slash-commands' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The Mattermost token'
- }
- ],
- 'slack-slash-commands' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The Slack token'
- }
- ],
- 'packagist' => [
- {
- required: true,
- name: :username,
- type: String,
- desc: 'The username'
- },
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The Packagist API token'
- },
- {
- required: false,
- name: :server,
- type: String,
- desc: 'The server'
- }
- ],
- 'pipelines-email' => [
- {
- required: true,
- name: :recipients,
- type: String,
- desc: 'Comma-separated list of recipient email addresses'
- },
- {
- required: false,
- name: :notify_only_broken_pipelines,
- type: Boolean,
- desc: 'Notify only broken pipelines'
- }
- ],
- 'pivotaltracker' => [
- {
- required: true,
- name: :token,
- type: String,
- desc: 'The Pivotaltracker token'
- },
- {
- required: false,
- name: :restrict_to_branch,
- type: String,
- desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
- }
- ],
- 'prometheus' => [
- {
- required: true,
- name: :api_url,
- type: String,
- desc: 'Prometheus API Base URL, like http://prometheus.example.com/'
- }
- ],
- 'pushover' => [
- {
- required: true,
- name: :api_key,
- type: String,
- desc: 'The application key'
- },
- {
- required: true,
- name: :user_key,
- type: String,
- desc: 'The user key'
- },
- {
- required: true,
- name: :priority,
- type: String,
- desc: 'The priority'
- },
- {
- required: true,
- name: :device,
- type: String,
- desc: 'Leave blank for all active devices'
- },
- {
- required: true,
- name: :sound,
- type: String,
- desc: 'The sound of the notification'
- }
- ],
- 'redmine' => [
- {
- required: true,
- name: :new_issue_url,
- type: String,
- desc: 'The new issue URL'
- },
- {
- required: true,
- name: :project_url,
- type: String,
- desc: 'The project URL'
- },
- {
- required: true,
- name: :issues_url,
- type: String,
- desc: 'The issues URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'The description of the tracker'
- }
- ],
- 'youtrack' => [
- {
- required: true,
- name: :project_url,
- type: String,
- desc: 'The project URL'
- },
- {
- required: true,
- name: :issues_url,
- type: String,
- desc: 'The issues URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'The description of the tracker'
- }
- ],
- 'slack' => [
- CHAT_NOTIFICATION_SETTINGS,
- CHAT_NOTIFICATION_FLAGS,
- CHAT_NOTIFICATION_CHANNELS,
- CHAT_NOTIFICATION_EVENTS
- ].flatten,
- 'microsoft-teams' => [
- {
- required: true,
- name: :webhook,
- type: String,
- desc: 'The Microsoft Teams webhook. e.g. https://outlook.office.com/webhook/…'
- }
- ],
- 'mattermost' => [
- CHAT_NOTIFICATION_SETTINGS,
- CHAT_NOTIFICATION_FLAGS,
- CHAT_NOTIFICATION_CHANNELS,
- CHAT_NOTIFICATION_EVENTS
- ].flatten,
- 'teamcity' => [
- {
- required: true,
- name: :teamcity_url,
- type: String,
- desc: 'TeamCity root URL like https://teamcity.example.com'
- },
- {
- required: true,
- name: :build_type,
- type: String,
- desc: 'Build configuration ID'
- },
- {
- required: true,
- name: :username,
- type: String,
- desc: 'A user with permissions to trigger a manual build'
- },
- {
- required: true,
- name: :password,
- type: String,
- desc: 'The password of the user'
- }
- ]
- }
-
- service_classes = [
- AsanaService,
- AssemblaService,
- BambooService,
- BugzillaService,
- BuildkiteService,
- CampfireService,
- CustomIssueTrackerService,
- DiscordService,
- DroneCiService,
- EmailsOnPushService,
- ExternalWikiService,
- FlowdockService,
- HangoutsChatService,
- IrkerService,
- JiraService,
- KubernetesService,
- MattermostSlashCommandsService,
- SlackSlashCommandsService,
- PackagistService,
- PipelinesEmailService,
- PivotaltrackerService,
- PrometheusService,
- PushoverService,
- RedmineService,
- YoutrackService,
- SlackService,
- MattermostService,
- MicrosoftTeamsService,
- TeamcityService
- ]
+ services = Helpers::ServicesHelpers.services
+ service_classes = Helpers::ServicesHelpers.service_classes
if Rails.env.development?
services['mock-ci'] = [
@@ -704,11 +16,7 @@ module API
services['mock-deployment'] = []
services['mock-monitoring'] = []
- service_classes += [
- MockCiService,
- MockDeploymentService,
- MockMonitoringService
- ]
+ service_classes += Helpers::ServicesHelpers.development_service_classes
end
SERVICES = services.freeze
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index b16faffe335..3cb2f69c4ef 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -9,6 +9,11 @@ module API
@current_setting ||=
(ApplicationSetting.current_without_cache || ApplicationSetting.create_from_defaults)
end
+
+ def filter_attributes_using_license(attrs)
+ # This method will be redefined in EE.
+ attrs
+ end
end
desc 'Get the current application settings' do
@@ -156,6 +161,8 @@ module API
attrs[:password_authentication_enabled_for_web] = attrs.delete(:password_authentication_enabled)
end
+ attrs = filter_attributes_using_license(attrs)
+
if ApplicationSettings::UpdateService.new(current_settings, current_user, attrs).execute
present current_settings, with: Entities::ApplicationSetting
else
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 8fc7c7361e1..0e829c5699b 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -13,7 +13,7 @@ module API
end
params do
requires :ref, type: String, desc: 'The commit sha or name of a branch or tag', allow_blank: false
- requires :token, type: String, desc: 'The unique token of trigger'
+ requires :token, type: String, desc: 'The unique token of trigger or job token'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index 148deb86c4c..d0d81ebc870 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -7,6 +7,14 @@ module API
before { authenticate! }
before { authorize! :admin_build, user_project }
+ helpers do
+ def filter_variable_parameters(params)
+ # This method exists so that EE can more easily filter out certain
+ # parameters, without having to modify the source code directly.
+ params
+ end
+ end
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
@@ -50,6 +58,7 @@ module API
end
post ':id/variables' do
variable_params = declared_params(include_missing: false)
+ variable_params = filter_variable_parameters(variable_params)
variable = user_project.variables.create(variable_params)
@@ -75,6 +84,7 @@ module API
break not_found!('Variable') unless variable
variable_params = declared_params(include_missing: false).except(:key)
+ variable_params = filter_variable_parameters(variable_params)
if variable.update(variable_params)
present variable, with: Entities::Variable
diff --git a/lib/gitlab/background_migration/archive_legacy_traces.rb b/lib/gitlab/background_migration/archive_legacy_traces.rb
index 92096e29ef1..7ee783b8489 100644
--- a/lib/gitlab/background_migration/archive_legacy_traces.rb
+++ b/lib/gitlab/background_migration/archive_legacy_traces.rb
@@ -11,11 +11,10 @@ module Gitlab
# So we chose a way to use ::Ci::Build directly and we don't change the `archive!` method until 11.1
::Ci::Build.finished.without_archived_trace
.where(id: start_id..stop_id).find_each do |build|
- begin
- build.trace.archive!
- rescue => e
- Rails.logger.error "Failed to archive live trace. id: #{build.id} message: #{e.message}"
- end
+
+ build.trace.archive!
+ rescue => e
+ Rails.logger.error "Failed to archive live trace. id: #{build.id} message: #{e.message}"
end
end
end
diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
index 698f5e46c0c..48aa369705f 100644
--- a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
+++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
@@ -302,14 +302,12 @@ module Gitlab
ldap_identities = Identity.where("provider like 'ldap%'").where(id: start_id..end_id)
ldap_identities.each do |identity|
- begin
- identity.extern_uid = Gitlab::Auth::LDAP::DN.new(identity.extern_uid).to_normalized_s
- unless identity.save
- Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\". Skipping."
- end
- rescue Gitlab::Auth::LDAP::DN::FormatError => e
- Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping."
+ identity.extern_uid = Gitlab::Auth::LDAP::DN.new(identity.extern_uid).to_normalized_s
+ unless identity.save
+ Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\". Skipping."
end
+ rescue Gitlab::Auth::LDAP::DN::FormatError => e
+ Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping."
end
end
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb
index a19dc9747fb..755b5ee725a 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb
@@ -34,18 +34,16 @@ module Gitlab
def filter_error_files(files)
files.partition do |file|
- begin
- file.to_h
- true
- rescue => e
- msg = <<~MSG
+ file.to_h
+ true
+ rescue => e
+ msg = <<~MSG
Error parsing path "#{file.path}":
#{e.message}
#{e.backtrace.join("\n ")}
MSG
- Rails.logger.error(msg)
- false
- end
+ Rails.logger.error(msg)
+ false
end
end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 441fdec8a1e..769d3279f91 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -79,31 +79,29 @@ module Gitlab
create_labels
client.issues(repo).each do |issue|
- begin
- description = ''
- description += @formatter.author_line(issue.author) unless find_user_id(issue.author)
- description += issue.description
-
- label_name = issue.kind
- milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil
-
- gitlab_issue = project.issues.create!(
- iid: issue.iid,
- title: issue.title,
- description: description,
- state: issue.state,
- author_id: gitlab_user_id(project, issue.author),
- milestone: milestone,
- created_at: issue.created_at,
- updated_at: issue.updated_at
- )
-
- gitlab_issue.labels << @labels[label_name]
-
- import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted?
- rescue StandardError => e
- errors << { type: :issue, iid: issue.iid, errors: e.message }
- end
+ description = ''
+ description += @formatter.author_line(issue.author) unless find_user_id(issue.author)
+ description += issue.description
+
+ label_name = issue.kind
+ milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil
+
+ gitlab_issue = project.issues.create!(
+ iid: issue.iid,
+ title: issue.title,
+ description: description,
+ state: issue.state,
+ author_id: gitlab_user_id(project, issue.author),
+ milestone: milestone,
+ created_at: issue.created_at,
+ updated_at: issue.updated_at
+ )
+
+ gitlab_issue.labels << @labels[label_name]
+
+ import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted?
+ rescue StandardError => e
+ errors << { type: :issue, iid: issue.iid, errors: e.message }
end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -150,37 +148,35 @@ module Gitlab
pull_requests = client.pull_requests(repo)
pull_requests.each do |pull_request|
- begin
- description = ''
- description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
- description += pull_request.description
-
- source_branch_sha = pull_request.source_branch_sha
- target_branch_sha = pull_request.target_branch_sha
- source_branch_sha = project.repository.commit(source_branch_sha)&.sha || source_branch_sha
- target_branch_sha = project.repository.commit(target_branch_sha)&.sha || target_branch_sha
-
- merge_request = project.merge_requests.create!(
- iid: pull_request.iid,
- title: pull_request.title,
- description: description,
- source_project: project,
- source_branch: pull_request.source_branch_name,
- source_branch_sha: source_branch_sha,
- target_project: project,
- target_branch: pull_request.target_branch_name,
- target_branch_sha: target_branch_sha,
- state: pull_request.state,
- author_id: gitlab_user_id(project, pull_request.author),
- assignee_id: nil,
- created_at: pull_request.created_at,
- updated_at: pull_request.updated_at
- )
-
- import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
- rescue StandardError => e
- errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, trace: e.backtrace.join("\n"), raw_response: pull_request.raw }
- end
+ description = ''
+ description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
+ description += pull_request.description
+
+ source_branch_sha = pull_request.source_branch_sha
+ target_branch_sha = pull_request.target_branch_sha
+ source_branch_sha = project.repository.commit(source_branch_sha)&.sha || source_branch_sha
+ target_branch_sha = project.repository.commit(target_branch_sha)&.sha || target_branch_sha
+
+ merge_request = project.merge_requests.create!(
+ iid: pull_request.iid,
+ title: pull_request.title,
+ description: description,
+ source_project: project,
+ source_branch: pull_request.source_branch_name,
+ source_branch_sha: source_branch_sha,
+ target_project: project,
+ target_branch: pull_request.target_branch_name,
+ target_branch_sha: target_branch_sha,
+ state: pull_request.state,
+ author_id: gitlab_user_id(project, pull_request.author),
+ assignee_id: nil,
+ created_at: pull_request.created_at,
+ updated_at: pull_request.updated_at
+ )
+
+ import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, trace: e.backtrace.join("\n"), raw_response: pull_request.raw }
end
end
@@ -211,23 +207,21 @@ module Gitlab
end
inline_comments.each do |comment|
- begin
- attributes = pull_request_comment_attributes(comment)
- attributes[:discussion_id] = discussion_map[comment.parent_id] if comment.has_parent?
+ attributes = pull_request_comment_attributes(comment)
+ attributes[:discussion_id] = discussion_map[comment.parent_id] if comment.has_parent?
- attributes.merge!(
- position: position_map[comment.iid],
- type: 'DiffNote')
+ attributes.merge!(
+ position: position_map[comment.iid],
+ type: 'DiffNote')
- note = merge_request.notes.create!(attributes)
+ note = merge_request.notes.create!(attributes)
- # We can't store a discussion ID until a note is created, so if
- # replies are created before the parent the discussion ID won't be
- # linked properly.
- discussion_map[comment.iid] = note.discussion_id
- rescue StandardError => e
- errors << { type: :pull_request, iid: comment.iid, errors: e.message }
- end
+ # We can't store a discussion ID until a note is created, so if
+ # replies are created before the parent the discussion ID won't be
+ # linked properly.
+ discussion_map[comment.iid] = note.discussion_id
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: comment.iid, errors: e.message }
end
end
@@ -245,11 +239,9 @@ module Gitlab
def import_standalone_pr_comments(pr_comments, merge_request)
pr_comments.each do |comment|
- begin
- merge_request.notes.create!(pull_request_comment_attributes(comment))
- rescue StandardError => e
- errors << { type: :pull_request, iid: comment.iid, errors: e.message }
- end
+ merge_request.notes.create!(pull_request_comment_attributes(comment))
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: comment.iid, errors: e.message }
end
end
diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb
index 4a789ae457f..1d3ddeeb0f1 100644
--- a/lib/gitlab/bitbucket_server_import/importer.rb
+++ b/lib/gitlab/bitbucket_server_import/importer.rb
@@ -162,27 +162,23 @@ module Gitlab
restore_branches(batch) if recover_missing_commits
batch.each do |pull_request|
- begin
- import_bitbucket_pull_request(pull_request)
- rescue StandardError => e
- backtrace = Gitlab::Profiler.clean_backtrace(e.backtrace)
- log_error(stage: 'import_pull_requests', iid: pull_request.iid, error: e.message, backtrace: backtrace)
-
- errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, backtrace: backtrace.join("\n"), raw_response: pull_request.raw }
- end
+ import_bitbucket_pull_request(pull_request)
+ rescue StandardError => e
+ backtrace = Gitlab::Profiler.clean_backtrace(e.backtrace)
+ log_error(stage: 'import_pull_requests', iid: pull_request.iid, error: e.message, backtrace: backtrace)
+
+ errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, backtrace: backtrace.join("\n"), raw_response: pull_request.raw }
end
end
end
def delete_temp_branches
@temp_branches.each do |branch|
- begin
- client.delete_branch(project_key, repository_slug, branch.name, branch.sha)
- project.repository.delete_branch(branch.name)
- rescue BitbucketServer::Connection::ConnectionError => e
- log_error(stage: 'delete_temp_branches', branch: branch.name, error: e.message)
- @errors << { type: :delete_temp_branches, branch_name: branch.name, errors: e.message }
- end
+ client.delete_branch(project_key, repository_slug, branch.name, branch.sha)
+ project.repository.delete_branch(branch.name)
+ rescue BitbucketServer::Connection::ConnectionError => e
+ log_error(stage: 'delete_temp_branches', branch: branch.name, error: e.message)
+ @errors << { type: :delete_temp_branches, branch_name: branch.name, errors: e.message }
end
end
@@ -323,16 +319,14 @@ module Gitlab
def import_standalone_pr_comments(pr_comments, merge_request)
pr_comments.each do |comment|
- begin
- merge_request.notes.create!(pull_request_comment_attributes(comment))
+ merge_request.notes.create!(pull_request_comment_attributes(comment))
- comment.comments.each do |replies|
- merge_request.notes.create!(pull_request_comment_attributes(replies))
- end
- rescue StandardError => e
- log_error(stage: 'import_standalone_pr_comments', merge_request_id: merge_request.id, comment_id: comment.id, error: e.message)
- errors << { type: :pull_request, comment_id: comment.id, errors: e.message }
+ comment.comments.each do |replies|
+ merge_request.notes.create!(pull_request_comment_attributes(replies))
end
+ rescue StandardError => e
+ log_error(stage: 'import_standalone_pr_comments', merge_request_id: merge_request.id, comment_id: comment.id, error: e.message)
+ errors << { type: :pull_request, comment_id: comment.id, errors: e.message }
end
end
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index d45a044686e..7011dd1aaf2 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -98,7 +98,7 @@ module Gitlab
def read_uint32(gz)
binary = gz.read(4)
- binary.unpack('L>')[0] if binary
+ binary.unpack1('L>') if binary
end
def read_string(gz)
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 6c99e20e7af..cb9b1484dc2 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -262,7 +262,7 @@ review:
- persist_environment_url
environment:
name: review/$CI_COMMIT_REF_NAME
- url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+ url: http://$CI_PROJECT_ID-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN
on_stop: stop_review
artifacts:
paths: [environment_url.txt]
@@ -780,18 +780,18 @@ rollout 100%:
function install_dependencies() {
apk add -U openssl curl tar gzip bash ca-certificates git
- curl -L -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
- curl -L -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk
+ curl -sSL -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
+ curl -sSL -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk
apk add glibc-2.28-r0.apk
rm glibc-2.28-r0.apk
- curl "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx
+ curl -sS "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx
mv linux-amd64/helm /usr/bin/
mv linux-amd64/tiller /usr/bin/
helm version --client
tiller -version
- curl -L -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl"
+ curl -sSL -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl"
chmod +x /usr/bin/kubectl
kubectl version --client
}
diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb
index 4b822aa86c5..bfada512727 100644
--- a/lib/gitlab/danger/teammate.rb
+++ b/lib/gitlab/danger/teammate.rb
@@ -21,21 +21,21 @@ module Gitlab
# Traintainers also count as reviewers
def reviewer?(project, category)
- capabilities(project) == "reviewer #{category}" || traintainer?(project, category)
+ capabilities(project).include?("reviewer #{category}") || traintainer?(project, category)
end
def traintainer?(project, category)
- capabilities(project) == "trainee_maintainer #{category}"
+ capabilities(project).include?("trainee_maintainer #{category}")
end
def maintainer?(project, category)
- capabilities(project) == "maintainer #{category}"
+ capabilities(project).include?("maintainer #{category}")
end
private
def capabilities(project)
- projects.fetch(project, '')
+ Array(projects.fetch(project, []))
end
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index b6ca777e029..e2637ad602a 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -76,7 +76,7 @@ module Gitlab
postgresql? && version.to_f >= 9.4
end
- def self.pg_stat_wal_receiver_supported?
+ def self.postgresql_minimum_supported_version?
postgresql? && version.to_f >= 9.6
end
@@ -98,6 +98,10 @@ module Gitlab
Gitlab::Database.postgresql_9_or_less? ? 'pg_last_xlog_replay_location' : 'pg_last_wal_replay_lsn'
end
+ def self.pg_last_xact_replay_timestamp
+ 'pg_last_xact_replay_timestamp'
+ end
+
def self.nulls_last_order(field, direction = 'ASC')
order = "#{field} #{direction}"
diff --git a/lib/gitlab/database/count/reltuples_count_strategy.rb b/lib/gitlab/database/count/reltuples_count_strategy.rb
index c3a674aeb7e..695f6fa766e 100644
--- a/lib/gitlab/database/count/reltuples_count_strategy.rb
+++ b/lib/gitlab/database/count/reltuples_count_strategy.rb
@@ -37,6 +37,22 @@ module Gitlab
private
+ # Models using single-type inheritance (STI) don't work with
+ # reltuple count estimates. We just have to ignore them and
+ # use another strategy to compute them.
+ def non_sti_models
+ models.reject { |model| sti_model?(model) }
+ end
+
+ def non_sti_table_names
+ non_sti_models.map(&:table_name)
+ end
+
+ def sti_model?(model)
+ model.column_names.include?(model.inheritance_column) &&
+ model.base_class != model
+ end
+
def table_names
models.map(&:table_name)
end
@@ -47,7 +63,7 @@ module Gitlab
# 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|
+ get_statistics(non_sti_table_names, check_statistics: check_statistics).each_with_object({}) do |row, data|
model = table_to_model[row.table_name]
data[model] = row.estimate
end
diff --git a/lib/gitlab/database/count/tablesample_count_strategy.rb b/lib/gitlab/database/count/tablesample_count_strategy.rb
index fedf6ca4fe1..7777f31f702 100644
--- a/lib/gitlab/database/count/tablesample_count_strategy.rb
+++ b/lib/gitlab/database/count/tablesample_count_strategy.rb
@@ -48,12 +48,21 @@ module Gitlab
end
end
+ def where_clause(model)
+ return unless sti_model?(model)
+
+ "WHERE #{model.inheritance_column} = '#{model.name}'"
+ 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})
+ FROM #{model.table_name}
+ TABLESAMPLE SYSTEM (#{portion * 100})
+ REPEATABLE (0)
+ #{where_clause(model)}
SQL
rows = ActiveRecord::Base.connection.select_all(query)
diff --git a/lib/gitlab/database/multi_threaded_migration.rb b/lib/gitlab/database/multi_threaded_migration.rb
index 1d39a3d0b57..65a6cb8e369 100644
--- a/lib/gitlab/database/multi_threaded_migration.rb
+++ b/lib/gitlab/database/multi_threaded_migration.rb
@@ -35,12 +35,10 @@ module Gitlab
threads = Array.new(thread_count) do
Thread.new do
pool.with_connection do |connection|
- begin
- Thread.current[MULTI_THREAD_AR_CONNECTION] = connection
- yield
- ensure
- Thread.current[MULTI_THREAD_AR_CONNECTION] = nil
- end
+ Thread.current[MULTI_THREAD_AR_CONNECTION] = connection
+ yield
+ ensure
+ Thread.current[MULTI_THREAD_AR_CONNECTION] = nil
end
end
end
diff --git a/lib/gitlab/database/sha_attribute.rb b/lib/gitlab/database/sha_attribute.rb
index 8d97adaff99..109ae7893da 100644
--- a/lib/gitlab/database/sha_attribute.rb
+++ b/lib/gitlab/database/sha_attribute.rb
@@ -22,7 +22,7 @@ module Gitlab
# Casts binary data to a SHA1 in hexadecimal.
def deserialize(value)
value = super(value)
- value ? value.unpack(PACK_FORMAT)[0] : nil
+ value ? value.unpack1(PACK_FORMAT) : nil
end
# Casts a SHA1 in hexadecimal to the proper binary format.
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 491e4b47196..e5bbd500e98 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -314,11 +314,16 @@ module Gitlab
def tree_entry(path)
return unless path.present?
+ commit_tree_entry(path)
+ end
+
+ def commit_tree_entry(path)
# We're only interested in metadata, so limit actual data to 1 byte
# since Gitaly doesn't support "send no data" option.
entry = @repository.gitaly_commit_client.tree_entry(id, path, 1)
return unless entry
+ # To be compatible with the rugged format
entry = entry.to_h
entry.delete(:data)
entry[:name] = File.basename(path)
diff --git a/lib/gitlab/git/rugged_impl/commit.rb b/lib/gitlab/git/rugged_impl/commit.rb
index 251802878c3..f6777dfa0c3 100644
--- a/lib/gitlab/git/rugged_impl/commit.rb
+++ b/lib/gitlab/git/rugged_impl/commit.rb
@@ -43,6 +43,30 @@ module Gitlab
end
end
+ override :commit_tree_entry
+ def commit_tree_entry(path)
+ if Feature.enabled?(:rugged_commit_tree_entry)
+ rugged_tree_entry(path)
+ else
+ super
+ end
+ end
+
+ # Is this the same as Blob.find_entry_by_path ?
+ def rugged_tree_entry(path)
+ rugged_commit.tree.path(path)
+ rescue Rugged::TreeError
+ nil
+ end
+
+ def rugged_commit
+ @rugged_commit ||= if raw_commit.is_a?(Rugged::Commit)
+ raw_commit
+ else
+ @repository.rev_parse_target(id)
+ end
+ end
+
def init_from_rugged(commit)
author = commit.author
committer = commit.committer
diff --git a/lib/gitlab/git/rugged_impl/repository.rb b/lib/gitlab/git/rugged_impl/repository.rb
index fe0120b1199..c0a91f59ab9 100644
--- a/lib/gitlab/git/rugged_impl/repository.rb
+++ b/lib/gitlab/git/rugged_impl/repository.rb
@@ -12,7 +12,7 @@ module Gitlab
module Repository
extend ::Gitlab::Utils::Override
- FEATURE_FLAGS = %i(rugged_find_commit rugged_tree_entries rugged_tree_entry rugged_commit_is_ancestor).freeze
+ FEATURE_FLAGS = %i(rugged_find_commit rugged_tree_entries rugged_tree_entry rugged_commit_is_ancestor rugged_commit_tree_entry).freeze
def alternate_object_directories
relative_object_directories.map { |d| File.join(path, d) }
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 48c113a8b14..e3b9a7a1a89 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -75,13 +75,11 @@ module Gitlab
@certs = stub_cert_paths.flat_map do |cert_file|
File.read(cert_file).scan(PEM_REGEX).map do |cert|
- begin
- OpenSSL::X509::Certificate.new(cert).to_pem
- rescue OpenSSL::OpenSSLError => e
- Rails.logger.error "Could not load certificate #{cert_file} #{e}"
- Gitlab::Sentry.track_exception(e, extra: { cert_file: cert_file })
- nil
- end
+ OpenSSL::X509::Certificate.new(cert).to_pem
+ rescue OpenSSL::OpenSSLError => e
+ Rails.logger.error "Could not load certificate #{cert_file} #{e}"
+ Gitlab::Sentry.track_exception(e, extra: { cert_file: cert_file })
+ nil
end.compact
end.uniq.join("\n")
end
@@ -257,8 +255,7 @@ module Gitlab
# This is this actual number of times this call was made. Used for information purposes only
actual_call_count = increment_call_count("gitaly_#{call_site}_actual")
- # Do no enforce limits in production
- return if Rails.env.production? || ENV["GITALY_DISABLE_REQUEST_LIMITS"]
+ return unless enforce_gitaly_request_limits?
# Check if this call is nested within a allow_n_plus_1_calls
# block and skip check if it is
@@ -275,6 +272,19 @@ module Gitlab
raise TooManyInvocationsError.new(call_site, actual_call_count, max_call_count, max_stacks)
end
+ def self.enforce_gitaly_request_limits?
+ # We typically don't want to enforce request limits in production
+ # However, we have some production-like test environments, i.e., ones
+ # where `Rails.env.production?` returns `true`. We do want to be able to
+ # check if the limit is being exceeded while testing in those environments
+ # In that case we can use a feature flag to indicate that we do want to
+ # enforce request limits.
+ return true if feature_enabled?('enforce_requests_limits')
+
+ !(Rails.env.production? || ENV["GITALY_DISABLE_REQUEST_LIMITS"])
+ end
+ private_class_method :enforce_gitaly_request_limits?
+
def self.allow_n_plus_1_calls
return yield unless Gitlab::SafeRequestStore.active?
diff --git a/lib/gitlab/gitaly_client/blobs_stitcher.rb b/lib/gitlab/gitaly_client/blobs_stitcher.rb
index 01bab854082..f860d8ce517 100644
--- a/lib/gitlab/gitaly_client/blobs_stitcher.rb
+++ b/lib/gitlab/gitaly_client/blobs_stitcher.rb
@@ -13,17 +13,15 @@ module Gitlab
current_blob_data = nil
@rpc_response.each do |msg|
- begin
- if msg.oid.blank? && msg.data.blank?
- next
- elsif msg.oid.present?
- yield new_blob(current_blob_data) if current_blob_data
-
- current_blob_data = msg.to_h.slice(:oid, :path, :size, :revision, :mode)
- current_blob_data[:data] = msg.data.dup
- else
- current_blob_data[:data] << msg.data
- end
+ if msg.oid.blank? && msg.data.blank?
+ next
+ elsif msg.oid.present?
+ yield new_blob(current_blob_data) if current_blob_data
+
+ current_blob_data = msg.to_h.slice(:oid, :path, :size, :revision, :mode)
+ current_blob_data[:data] = msg.data.dup
+ else
+ current_blob_data[:data] << msg.data
end
end
diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb
index 7046b4e2a43..f5368d41629 100644
--- a/lib/gitlab/hashed_storage/migrator.rb
+++ b/lib/gitlab/hashed_storage/migrator.rb
@@ -81,8 +81,26 @@ module Gitlab
Rails.logger.error("#{err.message} rolling-back storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}")
end
+ # Returns whether we have any pending storage migration
+ #
+ def migration_pending?
+ any_non_empty_queue?(::HashedStorage::MigratorWorker, ::HashedStorage::ProjectMigrateWorker)
+ end
+
+ # Returns whether we have any pending storage rollback
+ #
+ def rollback_pending?
+ any_non_empty_queue?(::HashedStorage::RollbackerWorker, ::HashedStorage::ProjectRollbackWorker)
+ end
+
private
+ def any_non_empty_queue?(*workers)
+ workers.any? do |worker|
+ worker.jobs.any?
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def build_relation(start, finish)
relation = Project
diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb
index f3323c98af2..70b18221a66 100644
--- a/lib/gitlab/legacy_github_import/importer.rb
+++ b/lib/gitlab/legacy_github_import/importer.rb
@@ -89,12 +89,10 @@ module Gitlab
def import_labels
fetch_resources(:labels, repo, per_page: 100) do |labels|
labels.each do |raw|
- begin
- gh_label = LabelFormatter.new(project, raw)
- gh_label.create!
- rescue => e
- errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(gh_label.url), errors: e.message }
- end
+ gh_label = LabelFormatter.new(project, raw)
+ gh_label.create!
+ rescue => e
+ errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(gh_label.url), errors: e.message }
end
end
@@ -104,12 +102,10 @@ module Gitlab
def import_milestones
fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones|
milestones.each do |raw|
- begin
- gh_milestone = MilestoneFormatter.new(project, raw)
- gh_milestone.create!
- rescue => e
- errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(gh_milestone.url), errors: e.message }
- end
+ gh_milestone = MilestoneFormatter.new(project, raw)
+ gh_milestone.create!
+ rescue => e
+ errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(gh_milestone.url), errors: e.message }
end
end
end
@@ -223,24 +219,22 @@ module Gitlab
def create_comments(comments)
ActiveRecord::Base.no_touching do
comments.each do |raw|
- begin
- comment = CommentFormatter.new(project, raw, client)
+ comment = CommentFormatter.new(project, raw, client)
- # GH does not return info about comment's parent, so we guess it by checking its URL!
- *_, parent, iid = URI(raw.html_url).path.split('/')
+ # GH does not return info about comment's parent, so we guess it by checking its URL!
+ *_, parent, iid = URI(raw.html_url).path.split('/')
- issuable = if parent == 'issues'
- Issue.find_by(project_id: project.id, iid: iid)
- else
- MergeRequest.find_by(target_project_id: project.id, iid: iid)
- end
+ issuable = if parent == 'issues'
+ Issue.find_by(project_id: project.id, iid: iid)
+ else
+ MergeRequest.find_by(target_project_id: project.id, iid: iid)
+ end
- next unless issuable
+ next unless issuable
- issuable.notes.create!(comment.attributes)
- rescue => e
- errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
- end
+ issuable.notes.create!(comment.attributes)
+ rescue => e
+ errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
end
end
end
@@ -281,12 +275,10 @@ module Gitlab
def import_releases
fetch_resources(:releases, repo, per_page: 100) do |releases|
releases.each do |raw|
- begin
- gh_release = ReleaseFormatter.new(project, raw)
- gh_release.create! if gh_release.valid?
- rescue => e
- errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(gh_release.url), errors: e.message }
- end
+ gh_release = ReleaseFormatter.new(project, raw)
+ gh_release.create! if gh_release.valid?
+ rescue => e
+ errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(gh_release.url), errors: e.message }
end
end
end
diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb
index 0b04340fbb5..269d90fa971 100644
--- a/lib/gitlab/metrics/influx_db.rb
+++ b/lib/gitlab/metrics/influx_db.rb
@@ -52,10 +52,8 @@ module Gitlab
pool&.with do |connection|
prepared.each_slice(settings[:packet_size]) do |slice|
- begin
- connection.write_points(slice)
- rescue StandardError
- end
+ connection.write_points(slice)
+ rescue StandardError
end
end
rescue Errno::EADDRNOTAVAIL, SocketError => ex
diff --git a/lib/gitlab/middleware/basic_health_check.rb b/lib/gitlab/middleware/basic_health_check.rb
index acf8c301b8f..84e49805428 100644
--- a/lib/gitlab/middleware/basic_health_check.rb
+++ b/lib/gitlab/middleware/basic_health_check.rb
@@ -24,7 +24,13 @@ module Gitlab
def call(env)
return @app.call(env) unless env['PATH_INFO'] == HEALTH_PATH
- request = ActionDispatch::Request.new(env)
+ # We should be using ActionDispatch::Request instead of
+ # Rack::Request to be consistent with Rails, but due to a Rails
+ # bug described in
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/58573#note_149799010
+ # hosts behind a load balancer will only see 127.0.0.1 for the
+ # load balancer's IP.
+ request = Rack::Request.new(env)
return OK_RESPONSE if client_ip_whitelisted?(request)
diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb
index 6559c3e3c57..772d743c9b0 100644
--- a/lib/gitlab/recaptcha.rb
+++ b/lib/gitlab/recaptcha.rb
@@ -5,8 +5,8 @@ module Gitlab
def self.load_configurations!
if Gitlab::CurrentSettings.recaptcha_enabled
::Recaptcha.configure do |config|
- config.public_key = Gitlab::CurrentSettings.recaptcha_site_key
- config.private_key = Gitlab::CurrentSettings.recaptcha_private_key
+ config.site_key = Gitlab::CurrentSettings.recaptcha_site_key
+ config.secret_key = Gitlab::CurrentSettings.recaptcha_private_key
end
true
diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb
index d9811e036d3..f6d289476c5 100644
--- a/lib/gitlab/request_context.rb
+++ b/lib/gitlab/request_context.rb
@@ -13,7 +13,13 @@ module Gitlab
end
def call(env)
- req = ActionDispatch::Request.new(env)
+ # We should be using ActionDispatch::Request instead of
+ # Rack::Request to be consistent with Rails, but due to a Rails
+ # bug described in
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/58573#note_149799010
+ # hosts behind a load balancer will only see 127.0.0.1 for the
+ # load balancer's IP.
+ req = Rack::Request.new(env)
Gitlab::SafeRequestStore[:client_ip] = req.ip
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
index ed2c7ee9a2d..671d795ec33 100644
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb
@@ -63,7 +63,7 @@ module Gitlab
sleep(time)
Sidekiq.logger.warn "sending Sidekiq worker PGRP-#{pid} #{signal} (#{explanation})"
- Process.kill(signal, "-#{pid}")
+ Process.kill(signal, 0)
end
def wait_and_signal(time, signal, explanation)
diff --git a/lib/gitlab/sql/pattern.rb b/lib/gitlab/sql/pattern.rb
index 07d0acdbae9..b698391c8bd 100644
--- a/lib/gitlab/sql/pattern.rb
+++ b/lib/gitlab/sql/pattern.rb
@@ -23,8 +23,12 @@ module Gitlab
end
end
+ def min_chars_for_partial_matching
+ MIN_CHARS_FOR_PARTIAL_MATCHING
+ end
+
def partial_matching?(query)
- query.length >= MIN_CHARS_FOR_PARTIAL_MATCHING
+ query.length >= min_chars_for_partial_matching
end
# column - The column name to search in.
diff --git a/lib/gitlab/user_extractor.rb b/lib/gitlab/user_extractor.rb
index 874599688bb..b41d085ee77 100644
--- a/lib/gitlab/user_extractor.rb
+++ b/lib/gitlab/user_extractor.rb
@@ -16,6 +16,7 @@ module Gitlab
def users
return User.none unless @text.present?
+ return User.none if references.empty?
@users ||= User.from_union(union_relations)
end
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index e74ff6a9129..b5f99ea012b 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -10,6 +10,7 @@ module GoogleApi
class Client < GoogleApi::Auth
SCOPE = 'https://www.googleapis.com/auth/cloud-platform'.freeze
LEAST_TOKEN_LIFE_TIME = 10.minutes
+ CLUSTER_MASTER_AUTH_USERNAME = 'admin'.freeze
class << self
def session_key_for_token
@@ -64,6 +65,12 @@ module GoogleApi
"node_config": {
"machine_type": machine_type
},
+ "master_auth": {
+ "username": CLUSTER_MASTER_AUTH_USERNAME,
+ "client_certificate_config": {
+ issue_client_certificate: true
+ }
+ },
"legacy_abac": {
"enabled": legacy_abac
}
diff --git a/lib/tasks/gitlab/artifacts/migrate.rake b/lib/tasks/gitlab/artifacts/migrate.rake
index e7634d2ed4f..9012e55a70c 100644
--- a/lib/tasks/gitlab/artifacts/migrate.rake
+++ b/lib/tasks/gitlab/artifacts/migrate.rake
@@ -11,14 +11,13 @@ namespace :gitlab do
Ci::Build.joins(:project)
.with_artifacts_stored_locally
.find_each(batch_size: 10) do |build|
- begin
- build.artifacts_file.migrate!(ObjectStorage::Store::REMOTE)
- build.artifacts_metadata.migrate!(ObjectStorage::Store::REMOTE)
- logger.info("Transferred artifact ID #{build.id} with size #{build.artifacts_size} to object storage")
- rescue => e
- logger.error("Failed to transfer artifacts of #{build.id} with error: #{e.message}")
- end
+ build.artifacts_file.migrate!(ObjectStorage::Store::REMOTE)
+ build.artifacts_metadata.migrate!(ObjectStorage::Store::REMOTE)
+
+ logger.info("Transferred artifact ID #{build.id} with size #{build.artifacts_size} to object storage")
+ rescue => e
+ logger.error("Failed to transfer artifacts of #{build.id} with error: #{e.message}")
end
end
end
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 451ba651674..760331620ef 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -60,7 +60,7 @@ namespace :gitlab do
.new(server.storage)
.rename(path, new_path)
rescue StandardError => e
- puts "Error occured while moving the repository: #{e.message}".color(:red)
+ puts "Error occurred while moving the repository: #{e.message}".color(:red)
end
end
end
diff --git a/lib/tasks/gitlab/lfs/migrate.rake b/lib/tasks/gitlab/lfs/migrate.rake
index a45e5ca91e0..97c15175a23 100644
--- a/lib/tasks/gitlab/lfs/migrate.rake
+++ b/lib/tasks/gitlab/lfs/migrate.rake
@@ -9,13 +9,12 @@ namespace :gitlab do
LfsObject.with_files_stored_locally
.find_each(batch_size: 10) do |lfs_object|
- begin
- lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
- logger.info("Transferred LFS object #{lfs_object.oid} of size #{lfs_object.size.to_i.bytes} to object storage")
- rescue => e
- logger.error("Failed to transfer LFS object #{lfs_object.oid} with error: #{e.message}")
- end
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+
+ logger.info("Transferred LFS object #{lfs_object.oid} of size #{lfs_object.size.to_i.bytes} to object storage")
+ rescue => e
+ logger.error("Failed to transfer LFS object #{lfs_object.oid} with error: #{e.message}")
end
end
end
diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake
index a2136ce1b92..954f827f716 100644
--- a/lib/tasks/gitlab/storage.rake
+++ b/lib/tasks/gitlab/storage.rake
@@ -11,6 +11,13 @@ namespace :gitlab do
storage_migrator = Gitlab::HashedStorage::Migrator.new
helper = Gitlab::HashedStorage::RakeHelper
+ if storage_migrator.rollback_pending?
+ warn "There is already a rollback operation in progress, " \
+ "running a migration at the same time may have unexpected consequences."
+
+ next
+ end
+
if helper.range_single_item?
project = Project.with_unmigrated_storage.find_by(id: helper.range_from)
@@ -56,6 +63,13 @@ namespace :gitlab do
storage_migrator = Gitlab::HashedStorage::Migrator.new
helper = Gitlab::HashedStorage::RakeHelper
+ if storage_migrator.migration_pending?
+ warn "There is already a migration operation in progress, " \
+ "running a rollback at the same time may have unexpected consequences."
+
+ next
+ end
+
if helper.range_single_item?
project = Project.with_storage_feature(:repository).find_by(id: helper.range_from)
@@ -82,7 +96,6 @@ namespace :gitlab do
print "Enqueuing rollback of #{hashed_projects_count} projects in batches of #{helper.batch_size}"
helper.project_id_batches_rollback do |start, finish|
- puts "Start: #{start} FINISH: #{finish}"
storage_migrator.bulk_schedule_rollback(start: start, finish: finish)
print '.'
diff --git a/lib/tasks/gitlab/traces.rake b/lib/tasks/gitlab/traces.rake
index 5a232091a7e..5e1ec481ece 100644
--- a/lib/tasks/gitlab/traces.rake
+++ b/lib/tasks/gitlab/traces.rake
@@ -26,13 +26,12 @@ namespace :gitlab do
Ci::Build.joins(:project)
.with_archived_trace_stored_locally
.find_each(batch_size: 10) do |build|
- begin
- build.job_artifacts_trace.file.migrate!(ObjectStorage::Store::REMOTE)
- logger.info("Transferred job trace of #{build.id} to object storage")
- rescue => e
- logger.error("Failed to transfer artifacts of #{build.id} with error: #{e.message}")
- end
+ build.job_artifacts_trace.file.migrate!(ObjectStorage::Store::REMOTE)
+
+ logger.info("Transferred job trace of #{build.id} to object storage")
+ rescue => e
+ logger.error("Failed to transfer artifacts of #{build.id} with error: #{e.message}")
end
end
end
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index 5d673a1a285..c5d0f2c292f 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -19,11 +19,9 @@ unless Rails.env.production?
desc "GitLab | lint | Lint HAML files"
task :haml do
- begin
- Rake::Task['haml_lint'].invoke
- rescue RuntimeError # The haml_lint tasks raise a RuntimeError
- exit(1)
- end
+ Rake::Task['haml_lint'].invoke
+ rescue RuntimeError # The haml_lint tasks raise a RuntimeError
+ exit(1)
end
desc "GitLab | lint | Run several lint checks"
diff --git a/lib/tasks/migrate/migrate_iids.rake b/lib/tasks/migrate/migrate_iids.rake
index aa2d01730d7..cb7c496c31c 100644
--- a/lib/tasks/migrate/migrate_iids.rake
+++ b/lib/tasks/migrate/migrate_iids.rake
@@ -2,49 +2,43 @@ desc "GitLab | Build internal ids for issues and merge requests"
task migrate_iids: :environment do
puts 'Issues'.color(:yellow)
Issue.where(iid: nil).find_each(batch_size: 100) do |issue|
- begin
- issue.set_iid
+ issue.set_iid
- if issue.update_attribute(:iid, issue.iid)
- print '.'
- else
- print 'F'
- end
- rescue
+ if issue.update_attribute(:iid, issue.iid)
+ print '.'
+ else
print 'F'
end
+ rescue
+ print 'F'
end
puts 'done'
puts 'Merge Requests'.color(:yellow)
MergeRequest.where(iid: nil).find_each(batch_size: 100) do |mr|
- begin
- mr.set_iid
+ mr.set_iid
- if mr.update_attribute(:iid, mr.iid)
- print '.'
- else
- print 'F'
- end
- rescue
+ if mr.update_attribute(:iid, mr.iid)
+ print '.'
+ else
print 'F'
end
+ rescue
+ print 'F'
end
puts 'done'
puts 'Milestones'.color(:yellow)
Milestone.where(iid: nil).find_each(batch_size: 100) do |m|
- begin
- m.set_iid
+ m.set_iid
- if m.update_attribute(:iid, m.iid)
- print '.'
- else
- print 'F'
- end
- rescue
+ if m.update_attribute(:iid, m.iid)
+ print '.'
+ else
print 'F'
end
+ rescue
+ print 'F'
end
puts 'done'
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3a9c15c5314..2acb28ab30f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -984,15 +984,6 @@ msgstr ""
msgid "AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}"
msgstr ""
-msgid "AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}."
-msgstr ""
-
-msgid "AutoDevOps|add a Kubernetes cluster"
-msgstr ""
-
-msgid "AutoDevOps|enable Auto DevOps"
-msgstr ""
-
msgid "Automatically marked as default internal user"
msgstr ""
@@ -1356,6 +1347,9 @@ msgstr ""
msgid "CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly."
msgstr ""
+msgid "CICD|group enabled"
+msgstr ""
+
msgid "CICD|instance enabled"
msgstr ""
@@ -2336,6 +2330,9 @@ msgstr ""
msgid "Contribution"
msgstr ""
+msgid "Contribution Analytics"
+msgstr ""
+
msgid "Contribution Charts"
msgstr ""
@@ -3235,6 +3232,9 @@ msgstr ""
msgid "Environments|You don't have any environments right now"
msgstr ""
+msgid "Environments|protected"
+msgstr ""
+
msgid "Epic"
msgstr ""
@@ -3406,12 +3406,6 @@ msgstr ""
msgid "Except policy:"
msgstr ""
-msgid "Existing Git repository"
-msgstr ""
-
-msgid "Existing folder"
-msgstr ""
-
msgid "Existing members and groups"
msgstr ""
@@ -3834,6 +3828,9 @@ msgstr ""
msgid "Group ID"
msgstr ""
+msgid "Group ID: %{group_id}"
+msgstr ""
+
msgid "Group Runners"
msgstr ""
@@ -3867,18 +3864,33 @@ msgstr ""
msgid "Group: %{group_name}"
msgstr ""
+msgid "GroupSettings|Auto DevOps pipeline was updated for the group"
+msgstr ""
+
+msgid "GroupSettings|Auto DevOps will automatically build, test and deploy your application based on a predefined Continuous Integration and Delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end}"
+msgstr ""
+
msgid "GroupSettings|Badges"
msgstr ""
msgid "GroupSettings|Customize your group badges."
msgstr ""
+msgid "GroupSettings|Default to Auto DevOps pipeline for all projects within this group"
+msgstr ""
+
msgid "GroupSettings|Learn more about badges."
msgstr ""
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr ""
+msgid "GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found."
+msgstr ""
+
+msgid "GroupSettings|There was a problem updating Auto DevOps pipeline: %{error_messages}."
+msgstr ""
+
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
msgstr ""
@@ -4100,9 +4112,6 @@ msgstr ""
msgid "If enabled"
msgstr ""
-msgid "If you already have files you can push them using the %{link_to_cli} below."
-msgstr ""
-
msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
msgstr ""
@@ -4573,9 +4582,6 @@ msgstr ""
msgid "Learn more about Kubernetes"
msgstr ""
-msgid "Learn more about protected branches"
-msgstr ""
-
msgid "Learn more about signing commits"
msgstr ""
@@ -4962,6 +4968,9 @@ msgstr ""
msgid "Modal|Close"
msgstr ""
+msgid "Modify commit message"
+msgstr ""
+
msgid "Modify commit messages"
msgstr ""
@@ -5255,9 +5264,6 @@ msgstr ""
msgid "Not started"
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 ""
@@ -5449,9 +5455,6 @@ msgstr ""
msgid "Other Labels"
msgstr ""
-msgid "Otherwise it is recommended you start with one of the options below."
-msgstr ""
-
msgid "Outbound requests"
msgstr ""
@@ -6316,6 +6319,12 @@ msgstr ""
msgid "Push"
msgstr ""
+msgid "Push an existing Git repository"
+msgstr ""
+
+msgid "Push an existing folder"
+msgstr ""
+
msgid "Push events"
msgstr ""
@@ -8843,6 +8852,12 @@ msgstr ""
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can also upload existing files from your computer using the instructions below."
+msgstr ""
+
+msgid "You can create files directly in GitLab using one of the following options."
+msgstr ""
+
msgid "You can easily contribute to them by requesting to join these groups."
msgstr ""
@@ -9047,9 +9062,6 @@ msgstr ""
msgid "branch name"
msgstr ""
-msgid "command line instructions"
-msgstr ""
-
msgid "commented on %{link_to_project}"
msgstr ""
@@ -9167,6 +9179,9 @@ msgstr ""
msgid "mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}."
msgstr ""
+msgid "mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}."
+msgstr ""
+
msgid "mrWidgetCommitsAdded|1 merge commit"
msgstr ""
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 67d9a556621..86b7048ad3d 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -13,7 +13,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: uk\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2019-02-11 08:11\n"
+"PO-Revision-Date: 2019-03-14 07:35\n"
msgid " Status"
msgstr " Статус"
@@ -36,17 +36,24 @@ msgstr[2] " покращилося на %d одиниць"
msgstr[3] " покращилося на %d одиниць"
msgid " or "
-msgstr ""
+msgstr " або "
msgid " or <#epic id>"
-msgstr ""
+msgstr " або <#epic id>"
msgid " or <#issue id>"
-msgstr ""
+msgstr " або <#issue id>"
msgid "\"%{query}\" in projects"
msgstr "\"%{query}\" в проектах"
+msgid "%d comment"
+msgid_plural "%d comments"
+msgstr[0] "%d коментар"
+msgstr[1] "%d коментарі"
+msgstr[2] "%d коментарів"
+msgstr[3] "%d коментарів"
+
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d коміт"
@@ -62,7 +69,7 @@ msgstr[2] "%d комітів позаду"
msgstr[3] "%d комітів позаду"
msgid "%d commits"
-msgstr ""
+msgstr "%d комітів"
msgid "%d exporter"
msgid_plural "%d exporters"
@@ -94,10 +101,10 @@ msgstr[3] "%d задач"
msgid "%d issue selected"
msgid_plural "%d issues selected"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d вибрана задача"
+msgstr[1] "%d вибрані задачі"
+msgstr[2] "%d вибраних задач"
+msgstr[3] "%d вибраних задач"
msgid "%d layer"
msgid_plural "%d layers"
@@ -156,12 +163,28 @@ msgstr "%{counter_storage} (%{counter_repositories} репозиторій, %{co
msgid "%{count} %{alerts}"
msgstr "%{count} %{alerts}"
-msgid "%{count} more"
+msgid "%{count} approval required from %{name}"
+msgid_plural "%{count} approvals required from %{name}"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "%{count} approvals from %{name}"
msgstr ""
+msgid "%{count} more"
+msgstr "%{count} більше"
+
msgid "%{count} more assignees"
msgstr "%{count} більше виконавців"
+msgid "%{count} of %{required} approvals from %{name}"
+msgstr ""
+
+msgid "%{count} of %{total}"
+msgstr "%{count} з %{total}"
+
msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] "%{count} участник"
@@ -182,18 +205,18 @@ msgstr "%{filePath} видалено"
msgid "%{firstLabel} +%{labelCount} more"
msgstr "%{firstLabel} +%{labelCount} більше"
-msgid "%{firstOption} +%{extraOptionCount} more"
-msgstr ""
-
msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
msgstr "%{group_docs_link_start}Групи%{group_docs_link_end} дозволяють вам керувати і взаємодіяти між кількома проектами. Члени групи мають доступ до усіх її проектів."
msgid "%{issuableType} will be removed! Are you sure?"
msgstr "%{issuableType} буде видалено! Ви впевнені?"
-msgid "%{link_start}Read more%{link_end} about role permissions"
+msgid "%{label_for_message} unavailable"
msgstr ""
+msgid "%{link_start}Read more%{link_end} about role permissions"
+msgstr "%{link_start}Читати більше%{link_end} про дозволи ролей"
+
msgid "%{loadingIcon} Started"
msgstr "%{loadingIcon} Початок"
@@ -213,31 +236,31 @@ msgid "%{percent}%% complete"
msgstr "%{percent}%% завершено"
msgid "%{state} epics"
-msgstr ""
+msgstr "%{state} епіки"
msgid "%{strong_start}%{branch_count}%{strong_end} Branch"
msgid_plural "%{strong_start}%{branch_count}%{strong_end} Branches"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%{strong_start}%{branch_count}%{strong_end} Гілка"
+msgstr[1] "%{strong_start}%{branch_count}%{strong_end} Гілки"
+msgstr[2] "%{strong_start}%{branch_count}%{strong_end} Гілок"
+msgstr[3] "%{strong_start}%{branch_count}%{strong_end} Гілок"
msgid "%{strong_start}%{commit_count}%{strong_end} Commit"
msgid_plural "%{strong_start}%{commit_count}%{strong_end} Commits"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%{strong_start}%{commit_count}%{strong_end} Коміт"
+msgstr[1] "%{strong_start}%{commit_count}%{strong_end} Коміти"
+msgstr[2] "%{strong_start}%{commit_count}%{strong_end} Комітів"
+msgstr[3] "%{strong_start}%{commit_count}%{strong_end} Комітів"
msgid "%{strong_start}%{human_size}%{strong_end} Files"
-msgstr ""
+msgstr "%{strong_start}%{human_size}%{strong_end} Файлів"
msgid "%{strong_start}%{tag_count}%{strong_end} Tag"
msgid_plural "%{strong_start}%{tag_count}%{strong_end} Tags"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%{strong_start}%{tag_count}%{strong_end} Тег"
+msgstr[1] "%{strong_start}%{tag_count}%{strong_end} Теги"
+msgstr[2] "%{strong_start}%{tag_count}%{strong_end} Тегів"
+msgstr[3] "%{strong_start}%{tag_count}%{strong_end} Тегів"
msgid "%{text} %{files}"
msgid_plural "%{text} %{files} files"
@@ -259,10 +282,10 @@ msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what infor
msgstr "%{usage_ping_link_start}Довідатись більше%{usage_ping_link_end} про те, якою інформацією Ви ділитесь із GitLab Inc."
msgid "%{user_name} profile page"
-msgstr ""
+msgstr "%{user_name} сторінка профілю"
msgid "(external source)"
-msgstr ""
+msgstr "(зовнішнє джерело)"
msgid "+ %{count} more"
msgstr "+ ще %{count}"
@@ -270,8 +293,11 @@ msgstr "+ ще %{count}"
msgid "+ %{moreCount} more"
msgstr "+ ще %{moreCount}"
+msgid "+%{extraOptionCount} more"
+msgstr "+%{extraOptionCount} більше"
+
msgid ", or "
-msgstr ""
+msgstr ", або "
msgid "- Runner is active and can process any new jobs"
msgstr "- Runner активний і може виконувати нові завдання"
@@ -296,6 +322,13 @@ msgstr[1] "%{count} %{type} зміни"
msgstr[2] "%{count} %{type} змін"
msgstr[3] "%{count} %{type} змін"
+msgid "1 Day"
+msgid_plural "%d Days"
+msgstr[0] "1 день"
+msgstr[1] "%d дні"
+msgstr[2] "%d днів"
+msgstr[3] "%d днів"
+
msgid "1 closed issue"
msgid_plural "%d closed issues"
msgstr[0] "1 закрита задача"
@@ -363,13 +396,13 @@ msgid "1st contribution!"
msgstr "Перший внесок!"
msgid "2FA"
-msgstr ""
+msgstr "двофакторна автентифікація"
msgid "2FA enabled"
-msgstr "Двоетапна аутентифікація увімкнена"
+msgstr "Двофакторна автентифікація увімкнена"
msgid "403|Please contact your GitLab administrator to get permission."
-msgstr ""
+msgstr "Будь ласка, зверніться до адміністратора GitLab, щоб отримати дозвіл."
msgid "403|You don't have the permission to access this page."
msgstr "У вас немає доступу до цієї сторінки."
@@ -411,11 +444,26 @@ msgid "<strong>%{pushes}</strong> pushes, more than <strong>%{commits}</strong>
msgstr "<strong>%{pushes}</strong> відправок (push), більше ніж <strong>%{commits}</strong> зафіксовано<strong>%{people}</strong> учасниками."
msgid "<strong>Deletes</strong> source branch"
-msgstr ""
+msgstr "<strong>Видаляє</strong> гілку-джерело"
msgid "A 'Runner' is a process which runs a job. You can set up as many Runners as you need."
msgstr "'Runner' — це процес, який виконує завдання. Ви можете створити потрібну кількість Runner'ів."
+msgid "A .NET Core console application template, customizable for any .NET Core project"
+msgstr ""
+
+msgid "A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
+msgstr ""
+
+msgid "A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
+msgstr ""
+
+msgid "A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
+msgstr ""
+
+msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
+msgstr ""
+
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Набір графіків відносно безперервної інтеграції"
@@ -431,9 +479,18 @@ msgstr "Учасник команди GitLab по боротьбі з поруш
msgid "A new branch will be created in your fork and a new merge request will be started."
msgstr "У вашому форку буде створено нову гілку, а також буде ініційований новий запит на злиття."
+msgid "A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
+msgstr ""
+
msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
msgstr "Проект — це місце де ви можете розміщувати свої файли (репозиторій), планувати роботу (задачі) і публікувати документацію (вікі), %{among_other_things_link}."
+msgid "A ready-to-go template for use with Android apps."
+msgstr ""
+
+msgid "A ready-to-go template for use with iOS Swift apps."
+msgstr ""
+
msgid "A regular expression that will be used to find the test coverage output in the job trace. Leave blank to disable"
msgstr "Регулярний вираз, який буде використовуватися для пошуку результатів покриття тестами в завданні. Залиште пустим для вимкнення"
@@ -459,7 +516,7 @@ msgid "Abuse reports"
msgstr "Звіти про зловживання"
msgid "Accept invitation"
-msgstr ""
+msgstr "Прийняти запрошення"
msgid "Accept terms"
msgstr "Прийняти умови"
@@ -498,41 +555,68 @@ msgid "Add"
msgstr "Додати"
msgid "Add CHANGELOG"
-msgstr ""
+msgstr "Додати список змін (CHANGELOG)"
msgid "Add CONTRIBUTING"
-msgstr ""
+msgstr "Додати CONTRIBUTING"
msgid "Add Group Webhooks and GitLab Enterprise Edition."
msgstr "Додайте групові веб-гуки та GitLab Enterprise Edition."
msgid "Add Jaeger URL"
-msgstr "Додати URL-адрес Jaeger"
+msgstr "Додати URL-адресу Jaeger"
msgid "Add Kubernetes cluster"
msgstr "Додати Kubernetes-кластер"
msgid "Add README"
-msgstr ""
+msgstr "Додати інструкцію (README)"
+
+msgid "Add a bullet list"
+msgstr "Додати ненумерований список"
msgid "Add a general comment to this %{noteable_name}."
-msgstr ""
+msgstr "Додайте загальний коментар до цього %{noteable_name}."
msgid "Add a homepage to your wiki that contains information about your project and GitLab will display it here instead of this message."
msgstr "Додати домашню сторінку в вікі, яка містить інформацію про ваш проект, і GitLab відображатиме його тут замість цього повідомлення."
+msgid "Add a link"
+msgstr "Додати посилання"
+
+msgid "Add a numbered list"
+msgstr "Додати нумерований список"
+
msgid "Add a table"
msgstr "Додати таблицю"
+msgid "Add a task list"
+msgstr "Додати список завдань"
+
msgid "Add additional text to appear in all email communications. %{character_limit} character limit"
msgstr "Створіть додатковий текст, який буде присутній у всіх повідомленнях електронної пошти. Максимальна кількість символів — %{character_limit}"
+msgid "Add approver(s)"
+msgstr "Додати затверджуючих осіб"
+
+msgid "Add approvers"
+msgstr "Додати затверджуючих осіб"
+
+msgid "Add bold text"
+msgstr "Додати жирний текст"
+
msgid "Add comment now"
msgstr "Додати коментар"
+msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface"
+msgstr ""
+
msgid "Add image comment"
msgstr "Додати коментар до зображення"
+msgid "Add italic text"
+msgstr "Додати курсивний текст"
+
msgid "Add license"
msgstr "Додати ліцензію"
@@ -549,23 +633,26 @@ msgid "Add reaction"
msgstr "Додати реакцію"
msgid "Add to project"
-msgstr ""
+msgstr "Додати до проекту"
msgid "Add to review"
msgstr "Додати до перевірки"
msgid "Add todo"
-msgstr "Додати задачу"
+msgstr "Додати нагадування"
msgid "Add user(s) to the group:"
msgstr "Додати користувачів до групу:"
msgid "Add users or groups who are allowed to approve every merge request"
-msgstr ""
+msgstr "Додайте користувачів або групи, яким дозволено затверджувати будь-який запит на злиття"
msgid "Add users to group"
msgstr "Додати користувача до групи"
+msgid "Added at"
+msgstr ""
+
msgid "Adding new applications is disabled in your GitLab instance. Please contact your GitLab administrator to get the permission"
msgstr "Додавання нових проектів для вашого інстансу GitLab заборонено. Зверніться до свого адміністратора GitLab, щоб отримати дозвіл"
@@ -612,40 +699,40 @@ msgid "AdminProjects|Delete project"
msgstr "Видалити проект"
msgid "AdminSettings|Auto DevOps domain"
-msgstr ""
+msgstr "Домен Auto DevOps"
msgid "AdminSettings|Environment variables are protected by default"
-msgstr ""
+msgstr "Змінні середовища є захищеними за замовчуванням"
msgid "AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages."
msgstr "Вкажіть домен, який буде використовуватися в проекті за замовчуванням для стадій Auto Review Apps і Auto Deploy."
msgid "AdminSettings|When creating a new environment variable it will be protected by default."
-msgstr ""
+msgstr "При створенні нової змінної середовища вона буде захищена за замовчуванням."
msgid "AdminUsers|2FA Disabled"
-msgstr ""
+msgstr "2FA вимкнено"
msgid "AdminUsers|2FA Enabled"
-msgstr ""
+msgstr "2FA увімкнено"
msgid "AdminUsers|Active"
-msgstr ""
+msgstr "Активні"
msgid "AdminUsers|Admin"
-msgstr ""
+msgstr "Адміністратор"
msgid "AdminUsers|Admins"
-msgstr ""
+msgstr "Адміністратори"
msgid "AdminUsers|Block user"
msgstr "Заблоквати користувача"
msgid "AdminUsers|Blocked"
-msgstr ""
+msgstr "Заблоковано"
msgid "AdminUsers|Cannot unblock LDAP blocked users"
-msgstr ""
+msgstr "Неможливо розблокувати користувачів заблокованих в LDAP"
msgid "AdminUsers|Delete User %{username} and contributions?"
msgstr "Видалити користувача %{username} та його внески?"
@@ -660,28 +747,28 @@ msgid "AdminUsers|Delete user and contributions"
msgstr "Видалити користувача і його внески"
msgid "AdminUsers|External"
-msgstr ""
+msgstr "Зовнішні"
msgid "AdminUsers|It's you!"
-msgstr ""
+msgstr "Це ви!"
msgid "AdminUsers|New user"
-msgstr ""
+msgstr "Новий користувач"
msgid "AdminUsers|No users found"
-msgstr ""
+msgstr "Користувачів не знайдено"
msgid "AdminUsers|Search by name, email or username"
-msgstr ""
+msgstr "Шукати за іменем, електронною поштою або іменем користувача"
msgid "AdminUsers|Search users"
-msgstr ""
+msgstr "Пошук користувачів"
msgid "AdminUsers|Send email to users"
-msgstr ""
+msgstr "Відправити повідмолення електронної пошти користувачам"
msgid "AdminUsers|Sort by"
-msgstr ""
+msgstr "Сортувати за"
msgid "AdminUsers|To confirm, type %{projectName}"
msgstr "Для підтвердження введіть %{projectName}"
@@ -690,9 +777,12 @@ msgid "AdminUsers|To confirm, type %{username}"
msgstr "Для підтвердження введіть %{username}"
msgid "AdminUsers|User will be blocked"
-msgstr ""
+msgstr "Користувач буде заблокований"
msgid "AdminUsers|Without projects"
+msgstr "Без проектів"
+
+msgid "Advanced"
msgstr ""
msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings."
@@ -701,6 +791,9 @@ msgstr "Додаткові дозволи, сховище великих фай
msgid "Advanced settings"
msgstr "Додаткові параметри"
+msgid "After a successful password update you will be redirected to login screen."
+msgstr ""
+
msgid "Alert"
msgid_plural "Alerts"
msgstr[0] "Попередження"
@@ -709,11 +802,14 @@ msgstr[2] "Попереджень"
msgstr[3] "Попереджень"
msgid "Alerts"
-msgstr ""
+msgstr "Попередження"
msgid "All"
msgstr "Всі"
+msgid "All Members"
+msgstr "Всі учасники"
+
msgid "All changes are committed"
msgstr "Всі зміни закомічені"
@@ -721,7 +817,7 @@ msgid "All features are enabled for blank projects, from templates, or when impo
msgstr "Всі функції для нових проектів беруться із шаблонів або під час імпортування, але ви можете вимикати їх пізніше в налаштуваннях проекту."
msgid "All issues for this milestone are closed. You may close this milestone now."
-msgstr ""
+msgstr "Всі задачі для цього етапу закриті. Ви можете закрити цей етап."
msgid "All users"
msgstr "Всі користувачі"
@@ -744,6 +840,9 @@ msgstr "Дозволити відображення діаграм PlantUML в
msgid "Allow requests to the local network from hooks and services."
msgstr "Дозволити запити до локальної мережі із гуків та сервісів."
+msgid "Allow this key to push to repository as well? (Default only allows pull access.)"
+msgstr ""
+
msgid "Allow users to request access"
msgstr "Дозволити користувачам запитувати доступ"
@@ -783,9 +882,6 @@ msgstr "Порожнє поле Gitlab-користувача буде запо
msgid "An error has occurred"
msgstr "Трапилася помилка"
-msgid "An error occured while fetching the releases. Please try again."
-msgstr ""
-
msgid "An error occurred adding a draft to the discussion."
msgstr "Виникла помилка під час додавання чернетки до обговорення."
@@ -793,6 +889,15 @@ msgid "An error occurred adding a new draft."
msgstr "Виникла помилка під час додавання нової чернетки."
msgid "An error occurred creating the new branch."
+msgstr "Помилка при створенні нової гілки."
+
+msgid "An error occurred fetching the approval rules."
+msgstr ""
+
+msgid "An error occurred fetching the approvers for the new rule."
+msgstr "Помилка при отриманні затверджуючих осіб для нового правила."
+
+msgid "An error occurred fetching the dropdown data."
msgstr ""
msgid "An error occurred previewing the blob"
@@ -805,7 +910,10 @@ msgid "An error occurred when updating the issue weight"
msgstr "Збій під час оновлення ваги задачі"
msgid "An error occurred while adding approver"
-msgstr "Помилка при додаванні учасника для затвердження"
+msgstr "Помилка при додаванні затверджуючої особи"
+
+msgid "An error occurred while deleting the approvers group"
+msgstr "Помилка при видаленні групи затверджуючих осіб"
msgid "An error occurred while deleting the comment"
msgstr "Сталася помилка під час видалення коментаря"
@@ -843,6 +951,9 @@ msgstr "Помилка при отриманні завдань."
msgid "An error occurred while fetching the pipeline."
msgstr "Помилка при отриманні данних конвеєра."
+msgid "An error occurred while fetching the releases. Please try again."
+msgstr ""
+
msgid "An error occurred while getting projects"
msgstr "Сталася помилка при отриманні проектів"
@@ -858,6 +969,9 @@ msgstr "Виникла помилка під час завантаження д
msgid "An error occurred while loading commit signatures"
msgstr "Трапилася помилка при завантаженні сигнатур коміту"
+msgid "An error occurred while loading designs. Please try again."
+msgstr ""
+
msgid "An error occurred while loading diff"
msgstr "Помилка при завантаженні разниці (diff)"
@@ -868,19 +982,19 @@ msgid "An error occurred while loading the file"
msgstr "Помилка при завантаженні файлу"
msgid "An error occurred while loading the subscription details."
-msgstr ""
+msgstr "Помилка при завантаженні даних підписки."
msgid "An error occurred while making the request."
msgstr "Помилка при створенні запиту."
msgid "An error occurred while removing approver"
-msgstr "Помилка при видаленні учасника для затвердження"
+msgstr "Помилка при видаленні затверджуючої особи"
msgid "An error occurred while removing epics."
-msgstr ""
+msgstr "Помилка при видаленні епіків."
msgid "An error occurred while removing issues."
-msgstr ""
+msgstr "Помилка при видаленні задач."
msgid "An error occurred while rendering KaTeX"
msgstr "Сталася помилка при рендерингу KaTeX"
@@ -900,12 +1014,18 @@ msgstr "Помилка при збереженні статусу перевиз
msgid "An error occurred while saving assignees"
msgstr "Помилка при збереженні виконавців"
+msgid "An error occurred while saving the approval settings"
+msgstr ""
+
msgid "An error occurred while subscribing to notifications."
msgstr "Помилка при підписці на сповіщення."
msgid "An error occurred while unsubscribing to notifications."
msgstr "Помилка при відписці від сповіщень."
+msgid "An error occurred while updating approvers"
+msgstr "Помилка при оновленні затверджуючих осіб"
+
msgid "An error occurred while updating the comment"
msgstr "Під час оновлення коментаря сталася помилка"
@@ -913,56 +1033,59 @@ msgid "An error occurred while validating username"
msgstr "Сталася помилка під час перевірки імені користувача"
msgid "An error occurred whilst committing your changes."
-msgstr ""
+msgstr "Помилка при коміті ваших змін."
msgid "An error occurred whilst fetching the job trace."
-msgstr ""
+msgstr "Помилка при отриманні логу завдання."
msgid "An error occurred whilst fetching the latest pipeline."
-msgstr ""
+msgstr "Помилка при отриманні останнього конвеєра."
msgid "An error occurred whilst loading all the files."
-msgstr ""
+msgstr "Помилка при завантаженні всіх файлів."
msgid "An error occurred whilst loading the file content."
-msgstr ""
+msgstr "Помилка при завантаженні вмісту файлу."
msgid "An error occurred whilst loading the file."
-msgstr ""
+msgstr "Помилка при завантаженні файлу."
msgid "An error occurred whilst loading the merge request changes."
-msgstr ""
+msgstr "Помилка при завантаженні змін запиту на злиття."
msgid "An error occurred whilst loading the merge request version data."
-msgstr ""
+msgstr "Помилка при завантаженні даних версії запиту на злиття."
msgid "An error occurred whilst loading the merge request."
-msgstr ""
+msgstr "Помилка при завантаженні запиту на злиття."
msgid "An error occurred whilst loading the pipelines jobs."
-msgstr ""
+msgstr "Помилка при завантаженні завдань конвеєру."
msgid "An error occurred. Please try again."
msgstr "Сталась помилка. Спробуйте ще раз."
msgid "An unexpected error occurred while checking the project environment."
-msgstr ""
+msgstr "Неочікувана помилка при перевірці середовища проекту."
msgid "An unexpected error occurred while checking the project runners."
-msgstr ""
+msgstr "Неочікувана помилка при перевірці runner'ів проекту."
msgid "An unexpected error occurred while communicating with the Web Terminal."
-msgstr ""
+msgstr "Неочікувана помилка з'єднання із Веб-терміналом."
msgid "An unexpected error occurred while starting the Web Terminal."
-msgstr ""
+msgstr "Неочікувана помилка при запуску Веб-терміналу."
msgid "An unexpected error occurred while stopping the Web Terminal."
-msgstr ""
+msgstr "Неочікувана помилка при зупинці Веб-терміналу."
msgid "Analytics"
msgstr "Аналітика"
+msgid "Ancestors"
+msgstr ""
+
msgid "Anonymous"
msgstr "Анонімно"
@@ -991,19 +1114,65 @@ msgid "Applications"
msgstr "Застосунки"
msgid "Applied"
-msgstr ""
+msgstr "Застосовано"
msgid "Apply suggestion"
+msgstr "Застосувати пропозицію"
+
+msgid "ApprovalRuleRemove|%d member"
+msgid_plural "ApprovalRuleRemove|%d members"
+msgstr[0] "%d учасник"
+msgstr[1] "%d учасника"
+msgstr[2] "%d учасників"
+msgstr[3] "%d учасників"
+
+msgid "ApprovalRuleRemove|Approvals from this member are not revoked."
+msgid_plural "ApprovalRuleRemove|Approvals from these members are not revoked."
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{nMembers}."
+msgstr "Ви збираєтеся видалити групу затверджуючих осіб %{name} яка налічує %{nMembers} учасників."
+
+msgid "ApprovalRuleSummary|%d member"
+msgid_plural "ApprovalRuleSummary|%d members"
+msgstr[0] "%d учасник"
+msgstr[1] "%d учасника"
+msgstr[2] "%d учасників"
+msgstr[3] "%d учасників"
+
+msgid "ApprovalRuleSummary|%{count} approval required from %{membersCount}"
+msgid_plural "ApprovalRuleSummary|%{count} approvals required from %{membersCount}"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "ApprovalRule|All members with Developer role or higher and code owners (if any)"
msgstr ""
-msgid "Approvals"
+msgid "ApprovalRule|Members"
+msgstr "Учасники"
+
+msgid "ApprovalRule|Name"
+msgstr "Ім'я"
+
+msgid "ApprovalRule|No. approvals required"
msgstr ""
-msgid "Approvals required"
+msgid "ApprovalRule|e.g. QA, Security, etc."
msgstr ""
+msgid "Approvals"
+msgstr "Затвердження"
+
+msgid "Approvals required"
+msgstr "Потрібні затвердження"
+
msgid "Approvers"
-msgstr ""
+msgstr "Затверджуючі особи"
msgid "Apr"
msgstr "квіт."
@@ -1018,7 +1187,7 @@ msgid "Archived projects"
msgstr "Заархівовані проекти"
msgid "Are you sure"
-msgstr ""
+msgstr "Ви впевнені"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Ви впевнені, що хочете видалити цей розклад для конвеєра?"
@@ -1030,7 +1199,7 @@ msgid "Are you sure you want to lose unsaved changes?"
msgstr "Ви впевнені, що бажаєте втратити незбережені зміни?"
msgid "Are you sure you want to lose your issue information?"
-msgstr ""
+msgstr "Ви впевнені, що хочете втратити інформацію про задачу?"
msgid "Are you sure you want to regenerate the public key? You will have to update the public key on the remote server before mirroring will work again."
msgstr "Ви впевнені, що хочете повторно згенерувати відкритий ключ? Вам доведеться оновити відкритий ключ на віддаленому сервері, перш ніж дзеркалювання запрацює знову."
@@ -1038,14 +1207,20 @@ msgstr "Ви впевнені, що хочете повторно згенеру
msgid "Are you sure you want to remove %{group_name}?"
msgstr "Ви впевнені, що хочете видалити %{group_name}?"
+msgid "Are you sure you want to remove approver %{name}"
+msgstr "Ви впевнені, що хочете видалити затверджуючу особу %{name}"
+
msgid "Are you sure you want to remove approver %{name}?"
-msgstr ""
+msgstr "Ви впевнені, що хочете видалити затверджуючу особу %{name}?"
+
+msgid "Are you sure you want to remove group %{name}"
+msgstr "Ви впевнені, що хочете видалити групу %{name}"
msgid "Are you sure you want to remove group %{name}?"
-msgstr ""
+msgstr "Ви впевнені, що хочете видалити групу %{name}?"
msgid "Are you sure you want to remove the attachment?"
-msgstr ""
+msgstr "Ви впевнені, що бажаєте видалити вкладення?"
msgid "Are you sure you want to remove this identity?"
msgstr "Ви впевнені, що хочете видалити цю ідентифікацію?"
@@ -1063,7 +1238,7 @@ msgid "Are you sure you want to unlock %{path_lock_path}?"
msgstr "Ви впевнені, що хочете розблокувати %{path_lock_path}?"
msgid "Are you sure you want to unsubscribe from the %{type}: %{link_to_noteable_text}?"
-msgstr ""
+msgstr "Ви дійсно бажаєте скасувати підписку на %{type}: %{link_to_noteable_text}?"
msgid "Are you sure?"
msgstr "Ви впевнені?"
@@ -1096,7 +1271,7 @@ msgid "Assign milestone"
msgstr "Призначити етап"
msgid "Assign some issues to this milestone."
-msgstr ""
+msgstr "Призначити деякі задачі до цього етапу."
msgid "Assign to"
msgstr "Призначити"
@@ -1125,9 +1300,12 @@ msgstr "Списки виконавців показують усі задачі
msgid "Assignee(s)"
msgstr "Виконавець(ці)"
-msgid "Attach a file"
+msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "Attach a file"
+msgstr "Прикріпити файл"
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Прикріпити файл за допомогою перетягування або %{upload_link}"
@@ -1140,9 +1318,6 @@ msgstr "серп."
msgid "August"
msgstr "серпень"
-msgid "Auth Token"
-msgstr ""
-
msgid "Authentication Log"
msgstr "Журнал автентифікації"
@@ -1159,7 +1334,7 @@ msgid "Authorization code:"
msgstr "Код авторизації:"
msgid "Authorization key"
-msgstr ""
+msgstr "Ключ авторизації"
msgid "Authorization was granted by entering your username and password in the application."
msgstr "Авторизація відбулася після вводу вашого імені та паролю у застосунку."
@@ -1222,25 +1397,25 @@ msgid "Automatically marked as default internal user"
msgstr "Автоматично позначено як внутрішній користувач за замовчуванням"
msgid "Automatically resolved"
-msgstr ""
+msgstr "Вирішено автоматично"
msgid "Available"
msgstr "Доступно"
msgid "Available group Runners: %{runners}"
-msgstr ""
+msgstr "Доступні групові Runner'и: %{runners}"
msgid "Available shared Runners:"
-msgstr ""
+msgstr "Доступні загальні Runner'и:"
msgid "Available specific runners"
-msgstr ""
+msgstr "Доступні спеціальні Runner’и"
msgid "Avatar for %{assigneeName}"
-msgstr ""
+msgstr "Аватар для %{assigneeName}"
msgid "Avatar for %{name}"
-msgstr ""
+msgstr "Аватар для %{name}"
msgid "Avatar will be removed. Are you sure?"
msgstr "Аватар буде видалено. Ви впевнені?"
@@ -1426,10 +1601,10 @@ msgid "Bitbucket import"
msgstr "Імпорт з Bitbucket"
msgid "Block"
-msgstr ""
+msgstr "Блок"
msgid "Blocked"
-msgstr ""
+msgstr "Заблокований"
msgid "Blog"
msgstr "Блог"
@@ -1594,34 +1769,34 @@ msgid "Browse files"
msgstr "Перегляд файлів"
msgid "Built-in"
-msgstr ""
+msgstr "Вбудований"
msgid "Business"
-msgstr ""
+msgstr "Бізнес"
msgid "Business metrics (Custom)"
msgstr "Бізнес метрики (Власні)"
msgid "By %{user_name}"
-msgstr ""
+msgstr "Від %{user_name}"
msgid "ByAuthor|by"
msgstr "від"
msgid "CHANGELOG"
-msgstr ""
+msgstr "Список змін (CHANGELOG)"
msgid "CI / CD"
msgstr "CI / CD"
msgid "CI / CD Charts"
-msgstr ""
+msgstr "Статистика CI / CD"
msgid "CI / CD Settings"
msgstr "Налаштування CI/CD"
msgid "CI Lint"
-msgstr ""
+msgstr "Перевірка CI конфігурації"
msgid "CI will run using the credentials assigned above."
msgstr "CI буде працювати з використанням облікових даних, визначених вище."
@@ -1669,6 +1844,9 @@ msgid "CICD|The Auto DevOps pipeline will run if no alternative CI configuration
msgstr "Конвеєр Auto DevOps буде запущено, якщо не буде знайдено альтернативного файлу конфігуріції CI."
msgid "CICD|You must add a %{kubernetes_cluster_start}Kubernetes cluster integration%{kubernetes_cluster_end} to this project with a domain in order for your deployment strategy to work correctly."
+msgstr "Ви повинні додати %{kubernetes_cluster_start}кластерну інтеграцію Kubernetes%{kubernetes_cluster_end} до цього проекту з доменом для коректної роботи стратегії розгортання."
+
+msgid "CICD|group enabled"
msgstr ""
msgid "CICD|instance enabled"
@@ -1681,14 +1859,17 @@ msgid "Callback URL"
msgstr "URL зворотнього виклику"
msgid "Can override approvers and approvals required per merge request"
-msgstr ""
+msgstr "Можна змінювати необхідні затвердження та затверджуючих осіб для запитів на злиття"
msgid "Can't find HEAD commit for this branch"
msgstr "Не можу знайти HEAD-коміт для цієї гілки"
-msgid "Canary Deployments is a popular CI strategy, where a small portion of the fleet is updated to the new version of your application."
+msgid "Can't remove group members without group managed account"
msgstr ""
+msgid "Canary Deployments is a popular CI strategy, where a small portion of the fleet is updated to the new version of your application."
+msgstr "Canary Deployments — це популярна стратегія безперервної інтеграції, коли нова версія вашого застосунку встановлюється на невелику кількість машин."
+
msgid "Cancel"
msgstr "Скасувати"
@@ -1701,11 +1882,14 @@ msgstr "Неможливо злити автоматично"
msgid "Cannot modify managed Kubernetes cluster"
msgstr "Неможливо змінити керований кластер Kubernetes"
-msgid "Certificate"
+msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded."
msgstr ""
+msgid "Certificate"
+msgstr "Сертифікат"
+
msgid "Certificate (PEM)"
-msgstr ""
+msgstr "Сертифікат (PEM)"
msgid "Certificate fingerprint"
msgstr "Відбиток сертифіката"
@@ -1714,7 +1898,7 @@ msgid "Change Weight"
msgstr "Змінити вагу"
msgid "Change permissions"
-msgstr ""
+msgstr "Змінити права доступу"
msgid "Change template"
msgstr "Змінити шаблон"
@@ -1737,6 +1921,9 @@ msgstr "Анулювати коміт"
msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
msgstr "Це створить новий коміт, щоб анулювати існуючі зміни."
+msgid "Changes"
+msgstr "Зміни"
+
msgid "Changes are shown as if the <b>source</b> revision was being merged into the <b>target</b> revision."
msgstr "Зміни відображаються так, ніби <b>редакція-джерело</b> була злита в <b>цільову редакцію</b>."
@@ -1749,17 +1936,20 @@ msgstr "Статистика"
msgid "Chat"
msgstr "Чат"
+msgid "Check again"
+msgstr "Перевірити знову"
+
msgid "Check the %{docs_link_start}documentation%{docs_link_end}."
msgstr "Перегляньте %{docs_link_start}документацію%{docs_link_end}."
msgid "Check your .gitlab-ci.yml"
-msgstr ""
+msgstr "Перевірте свій .gitlab-ci.yml"
msgid "Checking %{text} availability…"
msgstr "Перевірка доступності %{text}…"
msgid "Checking approval status"
-msgstr ""
+msgstr "Перевірка статусу затвердження"
msgid "Checking branch availability..."
msgstr "Перевірка доступності гілки..."
@@ -1783,7 +1973,7 @@ msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to s
msgstr "Виберіть гілку чи тег (напр. %{master}) або введіть коміт (напр. %{sha}) для перегляду змін або для створення запиту на злиття."
msgid "Choose a file"
-msgstr ""
+msgstr "Виберіть файл"
msgid "Choose a role permission"
msgstr ""
@@ -1806,21 +1996,21 @@ msgstr "Виберіть файл..."
msgid "Choose the top-level group for your repository imports."
msgstr "Оберіть групу найвищого рівня для імпорту репозиторіїв."
-msgid "Choose what content you want to see on a group’s overview page"
+msgid "Choose visibility level, enable/disable project features (issues, repository, wiki, snippets) and set permissions."
msgstr ""
-msgid "Choose which groups you wish to synchronize to this secondary node."
-msgstr "Виберіть групи для синхронізації на цей вторинний вузол."
+msgid "Choose what content you want to see on a group’s overview page"
+msgstr "Вибрати вміст оглядової сторінки групи"
msgid "Choose which repositories you want to connect and run CI/CD pipelines."
msgstr "Виберіть, які репозиторії ви хочете підключити і запустити конвеєри CI/CD."
-msgid "Choose which repositories you want to import."
-msgstr "Виберіть, які репозиторії ви хочете імпортувати."
-
msgid "Choose which shards you wish to synchronize to this secondary node."
msgstr "Виберіть сегменти для синхронізації на цей вторинний вузол."
+msgid "Choose your merge method, set up a default merge request description template."
+msgstr ""
+
msgid "CiStatusLabel|canceled"
msgstr "скасовано"
@@ -1903,7 +2093,7 @@ msgid "CiVariable|Create wildcard"
msgstr "Створити шаблон"
msgid "CiVariable|Error occurred while saving variables"
-msgstr ""
+msgstr "Помилка при збереженні змінних"
msgid "CiVariable|New environment"
msgstr "Нове середовище"
@@ -1924,10 +2114,10 @@ msgid "ClassificationLabelUnavailable|is unavailable: %{reason}"
msgstr "не доступно: %{reason}"
msgid "Clear"
-msgstr ""
+msgstr "Очистити"
msgid "Clear input"
-msgstr ""
+msgstr "Очистити ввід"
msgid "Clear search"
msgstr "Очистити пошук"
@@ -1963,22 +2153,25 @@ msgid "Client authentication key"
msgstr "Ключ автентифікації клієнта"
msgid "Client authentication key password"
-msgstr "Пароль ключа аутентифікації клієнта"
+msgstr "Пароль ключа автентифікації клієнта"
msgid "Clients"
msgstr "Клієнти"
msgid "Clone"
-msgstr ""
+msgstr "Клонувати"
msgid "Clone repository"
msgstr "Клонувати репозиторій"
msgid "Clone with %{http_label}"
+msgstr "Клонувати з %{http_label}"
+
+msgid "Clone with KRB5"
msgstr ""
msgid "Clone with SSH"
-msgstr ""
+msgstr "Клонувати з SSH"
msgid "Close"
msgstr "Закрити"
@@ -1987,22 +2180,19 @@ msgid "Close epic"
msgstr "Закрити епік"
msgid "Close milestone"
-msgstr ""
+msgstr "Закрити етап"
msgid "Closed"
msgstr "Закрито"
-msgid "Closed (moved)"
-msgstr ""
-
msgid "Closed issues"
msgstr "Закриті задачі"
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
-msgstr ""
+msgstr "%{custom_domain_start}Детальніше%{custom_domain_end}."
msgid "ClusterIntegration| can be used instead of a custom domain."
-msgstr ""
+msgstr "може використовуватися замість власного домену."
msgid "ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}"
msgstr ""
@@ -2010,9 +2200,6 @@ msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr "%{appList} були успішно встановлені на ваш Kubernetes-кластер"
-msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}."
-msgstr "%{boldNotice} Це додасть додаткові ресурси, такі як балансувальник навантаження, який може збільшити витрати в залежності від провайдера хостингу, на якому встановлено кластер Kubernetes. Якщо ви використовуєте Google Kubernetes Engine, ви можете %{pricingLink}."
-
msgid "ClusterIntegration|%{title} upgraded successfully."
msgstr ""
@@ -2034,20 +2221,17 @@ msgstr "Додавання інтеграції до вашої групи на
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr "Детальні налаштування інтеграції із цим Kubernetes-кластером"
-msgid "ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}"
-msgstr "Після встановлення Ingress, вам необхідно направити свій DNS на згенеровану зовнішню IP-адресу, щоб переглянути ваш застосунок після його розгортання. %{ingressHelpLink}"
-
msgid "ClusterIntegration|Alternatively"
msgstr ""
-msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
-msgstr "Помилка при отриманні зон проекту: %{error}"
-
msgid "ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later."
msgstr "Помилка під час з'єднання з Google Cloud API. Будь ласка, спробуйте знову пізніше."
+msgid "ClusterIntegration|An error occurred while trying to fetch project zones: %{error}"
+msgstr "Помилка при отриманні зон проекту: %{error}"
+
msgid "ClusterIntegration|An error occurred while trying to fetch your projects: %{error}"
-msgstr ""
+msgstr "Помилка при отриманні ваших проектів: %{error}"
msgid "ClusterIntegration|An error occurred while trying to fetch zone machine types: %{error}"
msgstr ""
@@ -2062,13 +2246,13 @@ msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluste
msgstr "Ви впевнені, що хочете видалити інтеграцію із цим Kubernetes-кластером? Це не призведе до видалення самого кластера."
msgid "ClusterIntegration|Base domain"
-msgstr ""
+msgstr "Основний домен"
msgid "ClusterIntegration|CA Certificate"
msgstr "Сертифікат центру сертифікації"
msgid "ClusterIntegration|Cert-Manager"
-msgstr ""
+msgstr "Cert-Manager"
msgid "ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates are valid and up-to-date."
msgstr ""
@@ -2082,6 +2266,9 @@ msgstr "Виберіть, які застосунки необхідно вст
msgid "ClusterIntegration|Choose which of your environments will use this cluster."
msgstr "Виберіть, яке із ваших середовищ буде використовувати цей кластер."
+msgid "ClusterIntegration|Cluster health"
+msgstr ""
+
msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters."
msgstr ""
@@ -2091,13 +2278,13 @@ msgstr "Скопіювати API URL"
msgid "ClusterIntegration|Copy CA Certificate"
msgstr "Скопіювати сертифікат центру сертифікації"
-msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
-msgstr "Копіювати IP-адресу Ingress в буфер обміну"
+msgid "ClusterIntegration|Copy Ingress Endpoint to clipboard"
+msgstr ""
msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard"
msgstr "Скопіювати ім’я хоста Jupyter в буфер обміну"
-msgid "ClusterIntegration|Copy Knative IP Address to clipboard"
+msgid "ClusterIntegration|Copy Knative Endpoint to clipboard"
msgstr ""
msgid "ClusterIntegration|Copy Kubernetes cluster name"
@@ -2178,14 +2365,14 @@ msgstr "Приховати"
msgid "ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{help_link_start}read this first%{help_link_end}."
msgstr "Якщо ви налаштовуєте декілька кластерів і використовуєте Auto DevOps, %{help_link_start}прочитайте для початку це%{help_link_end}."
-msgid "ClusterIntegration|In order to show the health of the cluster, we'll need to provision your cluster with Prometheus to collect the required data."
-msgstr "Для того, щоб показати працездатність кластера, нам потрібно буде забезпечити ваш кластер з Prometheus для збору необхідних даних."
+msgid "ClusterIntegration|In order to view the health of your cluster, you must first install Prometheus below."
+msgstr ""
msgid "ClusterIntegration|Ingress"
msgstr "Ingress"
-msgid "ClusterIntegration|Ingress IP Address"
-msgstr "Ingress IP-адреса"
+msgid "ClusterIntegration|Ingress Endpoint"
+msgstr ""
msgid "ClusterIntegration|Ingress gives you a way to route requests to services based on the request host or path, centralizing a number of services into a single entrypoint."
msgstr "Ingress дозволяє вам маршрутизувати запити до служб на основі запитаного хоста або шляху, об'єднуючи ряд сервісів в одну точку входу."
@@ -2193,15 +2380,18 @@ msgstr "Ingress дозволяє вам маршрутизувати запит
msgid "ClusterIntegration|Install"
msgstr "Встановити"
-msgid "ClusterIntegration|Install Prometheus"
-msgstr "Встановити Prometheus"
-
msgid "ClusterIntegration|Installed"
msgstr "Встановлений"
msgid "ClusterIntegration|Installing"
msgstr "Встановлення"
+msgid "ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{pricingLink}."
+msgstr ""
+
+msgid "ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}."
+msgstr ""
+
msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
msgstr "Інтеграція кластерної автоматизації Kubernetes"
@@ -2209,7 +2399,7 @@ msgid "ClusterIntegration|Integration status"
msgstr "Статус інтеграції"
msgid "ClusterIntegration|Issuer Email"
-msgstr ""
+msgstr "Електронна пошта емітента"
msgid "ClusterIntegration|Issuers represent a certificate authority. You must provide an email address for your Issuer. "
msgstr ""
@@ -2229,7 +2419,7 @@ msgstr "Knative"
msgid "ClusterIntegration|Knative Domain Name:"
msgstr "Доменне ім'я Knative:"
-msgid "ClusterIntegration|Knative IP Address:"
+msgid "ClusterIntegration|Knative Endpoint:"
msgstr ""
msgid "ClusterIntegration|Knative extends Kubernetes to provide a set of middleware components that are essential to build modern, source-centric, and container-based applications that can run anywhere: on premises, in the cloud, or even in a third-party data center."
@@ -2241,9 +2431,6 @@ msgstr "Kubernetes-кластер"
msgid "ClusterIntegration|Kubernetes cluster details"
msgstr "Параметри Kubernetes-кластера"
-msgid "ClusterIntegration|Kubernetes cluster health"
-msgstr "Стан Kubernetes-кластера"
-
msgid "ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine..."
msgstr "Kubernetes-кластер створюється на Google Kubernetes Engine..."
@@ -2272,7 +2459,7 @@ msgid "ClusterIntegration|Learn more about group Kubernetes clusters"
msgstr "Дізнайтеся більше про групові кластери Kubernetes"
msgid "ClusterIntegration|Let's Encrypt"
-msgstr ""
+msgstr "Let's Encrypt"
msgid "ClusterIntegration|Machine type"
msgstr "Тип машини"
@@ -2286,9 +2473,6 @@ msgstr "Управління"
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
msgstr "Керуйте вашим Kubernetes-кластером за допомогою %{link_gke}"
-msgid "ClusterIntegration|More information"
-msgstr "Додаткова інформація"
-
msgid "ClusterIntegration|No machine types matched your search"
msgstr "Жоден тип машин не відповідає вашому пошуку"
@@ -2301,9 +2485,6 @@ msgstr "Жоден проект не відповідає вашому пошу
msgid "ClusterIntegration|No zones matched your search"
msgstr "Жодна зона не відповідає вашому пошуку"
-msgid "ClusterIntegration|Note:"
-msgstr "Примітка:"
-
msgid "ClusterIntegration|Number of nodes"
msgstr "Кількість вузлів"
@@ -2313,8 +2494,8 @@ msgstr "Введіть інформацію про доступ до свого
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "Будь-ласка впевніться, що ваш обліковий запис Google задовольняє наступним вимогам:"
-msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed."
-msgstr "Направте ваш DNS на цю згенеровану IP-адресу, щоб отримати доступ до вашого додатку, після його розгортання."
+msgid "ClusterIntegration|Point a wildcard DNS to this generated endpoint in order to access your application after it has been deployed."
+msgstr ""
msgid "ClusterIntegration|Project cluster"
msgstr "Кластер проекту"
@@ -2352,7 +2533,7 @@ msgstr "При бажанні ви можете замінити це на ва
msgid "ClusterIntegration|Request to begin installing failed"
msgstr "Запит про початок встановлення не виконано"
-msgid "ClusterIntegration|Retry upgrade"
+msgid "ClusterIntegration|Retry update"
msgstr ""
msgid "ClusterIntegration|Save changes"
@@ -2397,9 +2578,6 @@ msgstr "Показати"
msgid "ClusterIntegration|Something went wrong on our end."
msgstr "Щось пішло не так з нашого боку."
-msgid "ClusterIntegration|Something went wrong when upgrading %{title}. Please check the logs and try again."
-msgstr ""
-
msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine"
msgstr "Помилка при створенні вашого Kubernetes-кластера на Google Kubernetes Engine"
@@ -2407,10 +2585,10 @@ msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr "Під час встановлення %{title} сталася помилка"
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain."
-msgstr ""
+msgstr "Зазначення домену дозволить вам використовувати фази Auto Review Apps та Auto Deploy для %{auto_devops_start}Auto DevOps%{auto_devops_end}. Домен повинен мати шаблон DNS, що задовільняє цей домен."
-msgid "ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
-msgstr "Відбувається призначення IP-адреси. Будь ласка, перевірте квоти вашого Kubernetes кластера на Google Kubernetes Engine, якщо це займає багато часу."
+msgid "ClusterIntegration|The endpoint is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
+msgstr ""
msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
msgstr "Цей обліковий запис повинен мати наступні права для створення Kubernetes-кластера в %{link_to_container_project}"
@@ -2418,12 +2596,21 @@ msgstr "Цей обліковий запис повинен мати насту
msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters."
msgstr "Цей параметр дозволить вам встановлювати застосунки на кластери RBAC."
+msgid "ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint."
+msgstr ""
+
msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr "Увімкнути/вимкнути Kubernetes-кластер"
msgid "ClusterIntegration|Token"
msgstr "Токен"
+msgid "ClusterIntegration|Update failed. Please check the logs and try again."
+msgstr ""
+
+msgid "ClusterIntegration|Updating"
+msgstr ""
+
msgid "ClusterIntegration|Upgrade"
msgstr ""
@@ -2460,9 +2647,6 @@ msgstr "Зона"
msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr "доступ до Google Kubernetes Engine"
-msgid "ClusterIntegration|check the pricing here"
-msgstr "переглянути ціни тут"
-
msgid "ClusterIntegration|documentation"
msgstr "документації"
@@ -2472,6 +2656,9 @@ msgstr "сторінка допомоги"
msgid "ClusterIntegration|meets the requirements"
msgstr "задовольняє вимогам"
+msgid "ClusterIntegration|pricing"
+msgstr ""
+
msgid "ClusterIntegration|properly configured"
msgstr "правильно налаштований"
@@ -2479,31 +2666,43 @@ msgid "ClusterIntegration|sign up"
msgstr "реєстрації"
msgid "Code"
+msgstr "Код"
+
+msgid "Code Owners"
+msgstr "Власники коду"
+
+msgid "Code owner approval is required"
msgstr ""
msgid "Code owners"
msgstr "Власники коду"
+msgid "CodeOwner|Pattern"
+msgstr ""
+
msgid "Cohorts"
msgstr "Когорти"
msgid "Collapse"
msgstr "Згорнути"
+msgid "Collapse approvers"
+msgstr "Згорнути список затверджуючих осіб"
+
msgid "Collapse sidebar"
msgstr "Згорнути панель"
msgid "Command line instructions"
-msgstr ""
+msgstr "Інструкції для командного рядка"
msgid "Comment"
msgstr "Коментар"
msgid "Comment & close %{noteable_name}"
-msgstr ""
+msgstr "Коментувати та закрити %{noteable_name}"
msgid "Comment & reopen %{noteable_name}"
-msgstr ""
+msgstr "Коментувати та повторно відкрити %{noteable_name}"
msgid "Comment & resolve discussion"
msgstr "Залишити коментар і завершити обговорення"
@@ -2525,19 +2724,19 @@ msgstr[2] "Комітів"
msgstr[3] "Комітів"
msgid "Commit %{commit_id}"
-msgstr ""
+msgstr "Коміт %{commit_id}"
msgid "Commit Message"
-msgstr "Коміт-повідомелння"
+msgstr "Повідомлення коміту"
msgid "Commit deleted"
-msgstr ""
+msgstr "Коміт видалено"
msgid "Commit duration in minutes for last 30 commits"
msgstr "Тривалість останніх 30 комітів у хвилинах"
msgid "Commit message"
-msgstr "Коміт-повідомлення"
+msgstr "Повідомлення коміту"
msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
msgstr "Статистика комітів для %{ref} %{start_time} - %{end_time}"
@@ -2597,7 +2796,7 @@ msgid "Compare Revisions"
msgstr "Порівняння редакцій"
msgid "Compare changes"
-msgstr ""
+msgstr "Порівняти зміни"
msgid "Compare changes with the last commit"
msgstr "Порівняти зміни з останнім комітом"
@@ -2627,7 +2826,7 @@ msgid "Confidentiality"
msgstr "Конфіденційність"
msgid "Configure GitLab runners to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}"
-msgstr ""
+msgstr "Налаштувати runner'ів GitLab для початку використання Веб-терміналу. %{helpStart}Докладніше.%{helpEnd}"
msgid "Configure Gitaly timeouts."
msgstr "Налаштувати таймаути Gitaly."
@@ -2636,7 +2835,7 @@ msgid "Configure Tracing"
msgstr "Налаштування Відстеження"
msgid "Configure a <code>.gitlab-webide.yml</code> file in the <code>.gitlab</code> directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}"
-msgstr ""
+msgstr "Налаштуйте файл <code>.gitlab-webide.yml</code> у директорії <code>.gitlab</code>, щоб почати використовувати Веб-термінал. %{helpStart}Докладніше.%{helpEnd}"
msgid "Configure automatic git checks and housekeeping on repositories."
msgstr "Налаштувати автоматичні перевірки git і очищення в репозиторіях."
@@ -2672,7 +2871,7 @@ msgid "Connecting..."
msgstr "З'єднання..."
msgid "Contact sales to upgrade"
-msgstr ""
+msgstr "Зверніться до відділу продажів для оновлення"
msgid "Container Registry"
msgstr "Реєстр Контейнерів"
@@ -2723,7 +2922,7 @@ msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access
msgstr "Ви також можете використовувати %{deploy_token} для доступу тільки для читання до образів у реєстрі."
msgid "Contents of .gitlab-ci.yml"
-msgstr ""
+msgstr "Вміст .gitlab-ci.yml"
msgid "Continue"
msgstr "Продовжити"
@@ -2741,7 +2940,7 @@ msgid "Contribution"
msgstr "Внесок"
msgid "Contribution Charts"
-msgstr ""
+msgstr "Статистика внесків"
msgid "Contributions for <strong>%{calendar_date}</strong>"
msgstr "Внески за <strong>%{calendar_date}</strong>"
@@ -2767,23 +2966,14 @@ msgstr "Будь ласка, зачекайте, ця сторінка авто
msgid "Control the display of third party offers."
msgstr "Керувати відображенням сторонніх пропозицій."
-msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
-msgstr "Задати максимальну кількість потоків для фонового завантаження LFS/вкладень для цього вторинного вузла"
-
msgid "Control the maximum concurrency of repository backfill for this secondary node"
msgstr "Задати максимальну кількість потоків для фонового завантаження репозиторіїв для цього вторинного вузла"
-msgid "Control the maximum concurrency of verification operations for this Geo node"
-msgstr "Встановіть максимальну кількість паралельних операцій перевірки для цього Geo-вузла"
-
-msgid "Control the minimum interval in days that a repository should be reverified for this primary node"
-msgstr ""
-
msgid "ConvDev Index"
msgstr "Індекс ConvDev"
msgid "Copy %{http_label} clone URL"
-msgstr ""
+msgstr "Скопіювати URL для клонування через %{http_label}"
msgid "Copy %{protocol} clone URL"
msgstr "Скопіювати URL для клонування через %{protocol}"
@@ -2791,11 +2981,14 @@ msgstr "Скопіювати URL для клонування через %{protoc
msgid "Copy ID to clipboard"
msgstr "Скопіювати ID в буфер обміну"
+msgid "Copy KRB5 clone URL"
+msgstr ""
+
msgid "Copy SSH clone URL"
msgstr "Скопіювати URL для клонування через SSH"
msgid "Copy SSH public key"
-msgstr ""
+msgstr "Скопіювати публічний ключ SSH"
msgid "Copy SSH public key to clipboard"
msgstr "Скопіюйте відкритий SSH-ключ в буфер обміну"
@@ -2843,7 +3036,7 @@ msgid "Create New Directory"
msgstr "Створити новий каталог"
msgid "Create New Domain"
-msgstr ""
+msgstr "Створити новий домен"
msgid "Create a new branch"
msgstr "Створити нову гілку"
@@ -2855,7 +3048,7 @@ msgid "Create a new issue"
msgstr "Створити нову задачу"
msgid "Create a new repository"
-msgstr ""
+msgstr "Створити новий репозиторій"
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "Створіть токен доступу для вашого аккаунта, щоб відправляти та отримувати через %{protocol}."
@@ -2884,9 +3077,6 @@ msgstr "Створити групу"
msgid "Create group label"
msgstr "Створити мітку групи"
-msgid "Create issue"
-msgstr "Створити задачу"
-
msgid "Create lists from labels. Issues with that label appear in that list."
msgstr "Створити список на основі міток. В ньому будуть задачі з такими мітками."
@@ -2897,7 +3087,7 @@ msgid "Create merge request and branch"
msgstr "Створити запит на злиття та гілку"
msgid "Create milestone"
-msgstr ""
+msgstr "Створити етап"
msgid "Create new branch"
msgstr "Створити нову гілку"
@@ -2956,6 +3146,9 @@ msgstr "Синтаксис Cron"
msgid "Current Branch"
msgstr "Поточна гілка"
+msgid "Current Project"
+msgstr "Поточний проект"
+
msgid "Current node"
msgstr "Поточний вузол"
@@ -2981,7 +3174,7 @@ msgid "Custom project templates"
msgstr "Власні шаблони проектів"
msgid "Custom project templates have not been set up for groups that you are a member of. They are enabled from a group’s settings page. Contact your group’s Owner or Maintainer to setup custom project templates."
-msgstr ""
+msgstr "Власні шаблони проектів не налаштовані для груп, до яких ви входите. Вони активуються на сторінці налаштувань групи. Зверніться до власника або керівника вашої групи для налаштування власних шаблонів проектів."
msgid "Customize colors"
msgstr "Налаштування кольорів"
@@ -2993,9 +3186,9 @@ msgid "Customize how Google Code email addresses and usernames are imported into
msgstr "Налаштуйте, як адреси електронної пошти та імена користувачів Google Code імпортуються в GitLab. На наступному кроці ви зможете вибрати проекти, які потрібно імпортувати."
msgid "Customize language and region related settings."
-msgstr ""
+msgstr "Налаштування мови і параметрів, пов'язаних із регіоном."
-msgid "Customize your merge request approval settings."
+msgid "Customize your issue restrictions."
msgstr ""
msgid "Customize your pipeline configuration, view your pipeline status and coverage report."
@@ -3029,7 +3222,7 @@ msgid "CycleAnalyticsStage|Test"
msgstr "Тестування"
msgid "DNS"
-msgstr ""
+msgstr "DNS"
msgid "Dashboard"
msgstr "Панель керування"
@@ -3041,7 +3234,7 @@ msgid "DashboardProjects|Personal"
msgstr "Особисті"
msgid "Data is still calculating..."
-msgstr ""
+msgstr "Дані все ще обчислюються..."
msgid "Date picker"
msgstr "Вибір дати"
@@ -3056,7 +3249,7 @@ msgid "December"
msgstr "грудень"
msgid "Decline"
-msgstr ""
+msgstr "Відхилити"
msgid "Decline and sign out"
msgstr "Відхити та вийти"
@@ -3068,10 +3261,10 @@ msgid "Default classification label"
msgstr "Мітка класифікації за замовчуванням"
msgid "Default first day of the week"
-msgstr ""
+msgstr "Перший день тижня за замовчуванням"
msgid "Default first day of the week in calendars and date pickers."
-msgstr ""
+msgstr "Перший день тижня за замовчуванням в календарях та при виборі дати."
msgid "Default: Directly import the Google Code email address or username"
msgstr "За замовчуванням: безпосередньо імпортувати адресу електронної пошти або ім'я користувача Google Code"
@@ -3083,7 +3276,7 @@ msgid "Define a custom pattern with cron syntax"
msgstr "Визначте власний шаблон за допомогою синтаксису cron"
msgid "Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here."
-msgstr ""
+msgstr "Визначте середовища на стадії розгортання у <code>.gitlab-ci.yml</code> щоб відстежувати розгортання тут."
msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes."
msgstr "Ви впевнені, що ви хочете запустити %{jobName} відразу? В іншому випадку це завдання буде виконано автоматично по завершенню таймера."
@@ -3116,10 +3309,10 @@ msgid "Delete list"
msgstr "Видалити список"
msgid "Delete source branch"
-msgstr ""
+msgstr "Видалити гілку-джерело"
msgid "Delete this attachment"
-msgstr ""
+msgstr "Видалити це вкладення"
msgid "Deleted"
msgstr "Видалено"
@@ -3258,7 +3451,7 @@ msgid "DeployTokens|Your new project deploy token has been created."
msgstr "Створено ваш новий токен розгортання для проекту."
msgid "Deployed"
-msgstr ""
+msgstr "Розгорнуто"
msgid "Deployed to"
msgstr "Розгорнуто на"
@@ -3281,6 +3474,9 @@ msgstr "Шаблони опису дозволяють визначити кон
msgid "Description:"
msgstr "Опис:"
+msgid "Designs"
+msgstr ""
+
msgid "Destroy"
msgstr "Знищити"
@@ -3288,7 +3484,7 @@ msgid "Details"
msgstr "Деталі"
msgid "Details (default)"
-msgstr ""
+msgstr "Деталі (за замовчуванням)"
msgid "Detect host keys"
msgstr "Виявлення ключів хоста"
@@ -3321,10 +3517,10 @@ msgid "Disable group Runners"
msgstr "Вимкнути групові Runner'и"
msgid "Disable shared Runners"
-msgstr ""
+msgstr "Вимкнути загальні Runner'и"
msgid "Disabled"
-msgstr ""
+msgstr "Вимкнено"
msgid "Discard"
msgstr "Відхилити"
@@ -3348,22 +3544,25 @@ msgid "Discard review"
msgstr "Відхилити перевірку"
msgid "Discover GitLab Geo"
-msgstr ""
+msgstr "Відкрийте GitLab Geo"
msgid "Discover projects, groups and snippets. Share your projects with others"
msgstr "Відкрийте для себе групи, проекти та фрагменти коду. Поділіться своїми проектами з іншими"
msgid "Discuss a specific suggestion or question"
-msgstr ""
+msgstr "Обговорити конкретну пропозицію чи питання"
msgid "Discuss a specific suggestion or question that needs to be resolved"
-msgstr ""
+msgstr "Обговорити конкретну пропозицію або питання, що необхідно вирішити"
+
+msgid "Discussion"
+msgstr "Обговорення"
msgid "Dismiss"
msgstr "Відхилити"
msgid "Dismiss ConvDev introduction"
-msgstr ""
+msgstr "Відхилити вступ до ConvDev"
msgid "Dismiss Cycle Analytics introduction box"
msgstr "Відхилити блок вступу до Аналитики Циклу"
@@ -3393,7 +3592,7 @@ msgid "Download"
msgstr "Завантажити"
msgid "Download artifacts"
-msgstr ""
+msgstr "Завантажити артефакти"
msgid "Download asset"
msgstr ""
@@ -3441,13 +3640,16 @@ msgid "Edit"
msgstr "Редагувати"
msgid "Edit %{name}"
-msgstr ""
+msgstr "Редагувати %{name}"
+
+msgid "Edit Deploy Key"
+msgstr "Редагувати ключ для розгортування"
msgid "Edit Label"
msgstr "Редагувати мітку"
msgid "Edit Milestone"
-msgstr ""
+msgstr "Редагувати етап"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Редагувати Розклад Конвеєра %{id}"
@@ -3459,10 +3661,13 @@ msgid "Edit application"
msgstr "Редагувати застосунок"
msgid "Edit comment"
-msgstr ""
+msgstr "Редагувати коментар"
msgid "Edit environment"
-msgstr ""
+msgstr "Редагувати середовище"
+
+msgid "Edit file"
+msgstr "Редагувати файл"
msgid "Edit files in the editor and commit changes here"
msgstr "Редагуйте файли в редакторі і закомітьте зміни тут"
@@ -3474,6 +3679,9 @@ msgid "Edit identity for %{user_name}"
msgstr "Редагувати ідентифікацію для %{user_name}"
msgid "Edit issues"
+msgstr "Редагувати задачі"
+
+msgid "Edit public deploy key"
msgstr ""
msgid "Elasticsearch"
@@ -3494,9 +3702,12 @@ msgstr "Адреси електронної пошти"
msgid "Embed"
msgstr "Вбудувати"
-msgid "Empty file"
+msgid "Emojis|Something went wrong while loading emojis."
msgstr ""
+msgid "Empty file"
+msgstr "Порожній файл"
+
msgid "Enable"
msgstr "Увімкнути"
@@ -3522,7 +3733,7 @@ msgid "Enable classification control using an external service"
msgstr "Увімкнути контроль за класифікацією за допомогою зовнішньої служби"
msgid "Enable error tracking"
-msgstr ""
+msgstr "Увімкнути відстеження помилок"
msgid "Enable for this project"
msgstr "Увімкнути для цього проекту"
@@ -3530,6 +3741,9 @@ msgstr "Увімкнути для цього проекту"
msgid "Enable group Runners"
msgstr "Увімкнути групові Runner'и"
+msgid "Enable header and footer in emails"
+msgstr ""
+
msgid "Enable or disable the Pseudonymizer data collection."
msgstr "Увімкнути чи вимкнути збір даних для Pseudonymizer."
@@ -3540,16 +3754,16 @@ msgid "Enable reCAPTCHA or Akismet and set IP limits."
msgstr "Увімкнути reCAPTCHA або Akismet та встановити обмеження по IP."
msgid "Enable self approval of merge requests"
-msgstr ""
+msgstr "Дозволити самостійне затвердження для запитів на злиття"
msgid "Enable shared Runners"
-msgstr ""
+msgstr "Увімкнути загальні Runner'и"
msgid "Enable the Performance Bar for a given group."
msgstr "Увімкнути панель продуктивності для даної групи."
msgid "Enable two-factor authentication"
-msgstr ""
+msgstr "Увімкнути двофакторну автентифікацію"
msgid "Enable usage ping"
msgstr "Увімкнути використання ping"
@@ -3566,6 +3780,9 @@ msgstr "Завершується о (за Грінвічем)"
msgid "Enforce SSO-only authentication for this group"
msgstr ""
+msgid "Enforce users to have dedicated group managed accounts for this group"
+msgstr ""
+
msgid "Enforced SSO"
msgstr ""
@@ -3584,26 +3801,23 @@ msgstr "Введіть опис запиту на злиття"
msgid "Enter the merge request title"
msgstr "Введіть назву запиту на злиття"
-msgid "Enter your Sentry API URL"
-msgstr ""
-
msgid "Environment variables"
-msgstr ""
+msgstr "Змінні середовища"
msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use environment variables for passwords, secret keys, or whatever you want."
msgstr ""
msgid "Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default"
-msgstr ""
+msgstr "Змінні середовища налаштовані адміністратором бути %{link_start}захищеними%{link_end} за замовчуванням"
msgid "Environment:"
-msgstr ""
+msgstr "Середовище:"
msgid "Environments"
msgstr "Середовища"
msgid "Environments allow you to track deployments of your application %{link_to_read_more}."
-msgstr ""
+msgstr "Середовища дозволяють відстежувати розгортання вашого застосунку %{link_to_read_more}."
msgid "Environments|An error occurred while fetching the environments."
msgstr "Виникла помилка при завантаженні середовищ."
@@ -3611,6 +3825,12 @@ msgstr "Виникла помилка при завантаженні серед
msgid "Environments|An error occurred while making the request."
msgstr "Під час виконання запиту сталася помилка."
+msgid "Environments|An error occurred while re-deploying the environment, please try again"
+msgstr ""
+
+msgid "Environments|An error occurred while rolling back the environment, please try again"
+msgstr ""
+
msgid "Environments|An error occurred while stopping the environment, please try again"
msgstr "Виникла помилка під час зупинки середовища, будь ласка, спробуйте ще раз"
@@ -3662,15 +3882,33 @@ msgstr "Відкрити працююче середовище"
msgid "Environments|Pod logs from"
msgstr "Журнал Pod’а"
+msgid "Environments|Re-deploy"
+msgstr "Повторно розгорнути"
+
+msgid "Environments|Re-deploy environment %{environment_name}?"
+msgstr ""
+
+msgid "Environments|Re-deploy environment %{name}?"
+msgstr ""
+
msgid "Environments|Re-deploy to environment"
msgstr "Повторно розгорнути в середовищі"
msgid "Environments|Read more about environments"
msgstr "Дізнайтеся більше про середовища"
+msgid "Environments|Rollback"
+msgstr ""
+
msgid "Environments|Rollback environment"
msgstr "Відкотити середовище"
+msgid "Environments|Rollback environment %{environment_name}?"
+msgstr ""
+
+msgid "Environments|Rollback environment %{name}?"
+msgstr ""
+
msgid "Environments|Show all"
msgstr "Показати всі"
@@ -3681,6 +3919,18 @@ msgid "Environments|Stop environment"
msgstr "Зупинити середовище"
msgid "Environments|Stopping"
+msgstr "Зупинка"
+
+msgid "Environments|This action will relaunch the job for commit %{commit_id}, putting the environment in a previous version. Are you sure you want to continue?"
+msgstr ""
+
+msgid "Environments|This action will relaunch the job for commit %{linkStart}%{commitId}%{linkEnd}, putting the environment in a previous version. Are you sure you want to continue?"
+msgstr ""
+
+msgid "Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
+msgstr ""
+
+msgid "Environments|This action will run the job defined by staging for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
msgstr ""
msgid "Environments|Updated"
@@ -3705,7 +3955,7 @@ msgid "Epics let you manage your portfolio of projects more efficiently and with
msgstr "Епіки дозволяють керувати вашим портфелем проектів ефективніше та з меншими зусиллями"
msgid "Epics|An error occurred while saving the %{epicDateType} date"
-msgstr ""
+msgstr "Сталася помилка при збереженні дати %{epicDateType}"
msgid "Epics|How can I solve this?"
msgstr "Як я можу це вирішити?"
@@ -3732,13 +3982,16 @@ msgid "Error Reporting and Logging"
msgstr "Звіти про помилки та логування"
msgid "Error Tracking"
-msgstr ""
+msgstr "Відстеження помилок"
+
+msgid "Error creating a new path"
+msgstr "Помилка при створенні нового шляху"
msgid "Error creating epic"
msgstr "Помилка при створенні епіку"
msgid "Error deleting %{issuableType}"
-msgstr ""
+msgstr "Помилка при видаленні %{issuableType}"
msgid "Error fetching contributors data."
msgstr "Помилка отримання даних учасників."
@@ -3783,29 +4036,56 @@ msgid "Error occurred when toggling the notification subscription"
msgstr "Сталася помилка під час підключення підписки на сповіщення"
msgid "Error rendering markdown preview"
-msgstr ""
+msgstr "Помилка при попередньому перегляді markdown"
msgid "Error saving label update."
msgstr "Помилка при збереженні мітки."
msgid "Error updating %{issuableType}"
-msgstr ""
+msgstr "Помилка при оновленні %{issuableType}"
msgid "Error updating status for all todos."
-msgstr "Помилка оновлення статусу для всіх задач."
+msgstr "Помилка оновлення статусу для всіх нагадувань."
msgid "Error updating todo status."
-msgstr "Помилка при оновленні статусу задачі."
+msgstr "Помилка при оновленні статусу нагадування."
msgid "Error while loading the merge request. Please try again."
msgstr "Помилка при завантаженні запита на злиття. Будь ласка, спробуйте знову."
msgid "Error:"
+msgstr "Помилка:"
+
+msgid "ErrorTracking|Active"
msgstr ""
-msgid "Errors"
+msgid "ErrorTracking|After adding your Auth Token, use the 'Connect' button to load projects"
msgstr ""
+msgid "ErrorTracking|Auth Token"
+msgstr ""
+
+msgid "ErrorTracking|Click 'Connect' to re-establish the connection to Sentry and activate the dropdown."
+msgstr ""
+
+msgid "ErrorTracking|Connection has failed. Re-check Auth Token and try again."
+msgstr ""
+
+msgid "ErrorTracking|Find your hostname in your Sentry account settings page"
+msgstr ""
+
+msgid "ErrorTracking|No projects available"
+msgstr ""
+
+msgid "ErrorTracking|Select project"
+msgstr ""
+
+msgid "ErrorTracking|To enable project selection, enter a valid Auth Token"
+msgstr ""
+
+msgid "Errors"
+msgstr "Помилки"
+
msgid "Estimated"
msgstr "За оцінками"
@@ -3828,10 +4108,10 @@ msgid "EventFilterBy|Filter by team"
msgstr "Фільтрувати по команді"
msgid "Events"
-msgstr ""
+msgstr "Події"
msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again."
-msgstr ""
+msgstr "Усі спроби %{action} закінчилися невдачею: %{job_error_message}. Будь ласка, спробуйте знову."
msgid "Every day (at 4:00am)"
msgstr "Кожен день (в 4:00 ранку)"
@@ -3843,37 +4123,37 @@ msgid "Every week (Sundays at 4:00am)"
msgstr "Щотижня (в неділю о 4:00 ранку)"
msgid "Everyone"
-msgstr ""
+msgstr "Будь-хто"
msgid "Everyone can contribute"
msgstr "Кожен може зробити свій внесок"
msgid "Everything you need to create a GitLab Pages site using GitBook."
-msgstr ""
+msgstr "Все, що потрібно для створення сайту на GitLab Pages за допомогою GitBook."
msgid "Everything you need to create a GitLab Pages site using Hexo."
-msgstr ""
+msgstr "Все, що потрібно для створення сайту на GitLab Pages за допомогою Hexo."
msgid "Everything you need to create a GitLab Pages site using Hugo."
-msgstr ""
+msgstr "Все, що потрібно для створення сайту на GitLab Pages за допомогою Hugo."
msgid "Everything you need to create a GitLab Pages site using Jekyll."
-msgstr ""
+msgstr "Все, що потрібно для створення сайту на GitLab Pages за допомогою Jekyll."
msgid "Everything you need to create a GitLab Pages site using plain HTML."
-msgstr ""
+msgstr "Все, що потрібно для створення сайту на GitLab Pages за допомогою простого HTML."
msgid "Except policy:"
msgstr ""
msgid "Existing Git repository"
-msgstr ""
+msgstr "Існуючий репозиторій Git"
msgid "Existing folder"
-msgstr ""
+msgstr "Існуюча папка"
msgid "Existing members and groups"
-msgstr ""
+msgstr "Існуючі учасники та групи"
msgid "Expand"
msgstr "Розгорнути"
@@ -3881,6 +4161,9 @@ msgstr "Розгорнути"
msgid "Expand all"
msgstr "Розгорнути все"
+msgid "Expand approvers"
+msgstr "Розгорнути список затверджуючих осіб"
+
msgid "Expand sidebar"
msgstr "Розгорніть бічну панель"
@@ -3915,22 +4198,22 @@ msgid "Explore public groups"
msgstr "Переглянути публічні групи"
msgid "Export as CSV"
-msgstr ""
+msgstr "Експортувати як CSV"
msgid "Export issues"
-msgstr ""
+msgstr "Експортувати задачі"
msgid "External Classification Policy Authorization"
msgstr "Зовнішня Класифікація Політики Авторизації"
msgid "External URL"
-msgstr ""
+msgstr "Зовнішній URL"
msgid "External Wiki"
-msgstr ""
+msgstr "Зовнішня вікі"
msgid "External authentication"
-msgstr "Зовнішня аутентифікація"
+msgstr "Зовнішня автентифікація"
msgid "External authorization denied access to this project"
msgstr "Зовнішня авторизація заборонила доступ до цього проекту"
@@ -3968,7 +4251,7 @@ msgstr "Не вдалося розгорнути до"
msgid "Failed to load emoji list."
msgstr "Не вдалося завантажити список смайликів."
-msgid "Failed to load errors from Sentry"
+msgid "Failed to load errors from Sentry. Error message: %{errorMessage}"
msgstr ""
msgid "Failed to remove issue from board, please try again."
@@ -4005,10 +4288,10 @@ msgid "Feature Flags"
msgstr "Перемикачі функцій"
msgid "FeatureFlags|* (All Environments)"
-msgstr ""
+msgstr "* (Усі середовища)"
msgid "FeatureFlags|* (All environments)"
-msgstr ""
+msgstr "* (всі середовища)"
msgid "FeatureFlags|API URL"
msgstr "URL-адреса API"
@@ -4026,49 +4309,46 @@ msgid "FeatureFlags|Create feature flag"
msgstr "Створити перемикач функції"
msgid "FeatureFlags|Delete %{name}?"
-msgstr ""
+msgstr "Видалити %{name}?"
msgid "FeatureFlags|Delete feature flag"
-msgstr ""
+msgstr "Вимкнути перемикач функції"
msgid "FeatureFlags|Description"
msgstr "Опис"
-msgid "FeatureFlags|Edit %{feature_flag_name}"
-msgstr "Редагувати %{feature_flag_name}"
-
msgid "FeatureFlags|Edit Feature Flag"
msgstr "Редагувати перемикач функції"
msgid "FeatureFlags|Environment Spec"
-msgstr ""
+msgstr "Специфікація середовища"
msgid "FeatureFlags|Environment Specs"
-msgstr ""
+msgstr "Специфікації середовищ"
msgid "FeatureFlags|Feature Flag"
msgstr "Перемикач функції"
msgid "FeatureFlags|Feature Flag behavior is built up by creating a set of rules to define the status of target environments. A default wildcare rule %{codeStart}*%{codeEnd} for %{boldStart}All Environments%{boldEnd} is set, and you are able to add as many rules as you need by choosing environment specs below. You can toggle the behavior for each of your rules to set them %{boldStart}Active%{boldEnd} or %{boldStart}Inactive%{boldEnd}."
-msgstr ""
+msgstr "Поведінка перемикачів функцій створюється шляхом створення набору правил для визначення стану цільових середовищ. Правило за замовчуванням %{codeStart}*%{codeEnd} для %{boldStart}Всіх середовищ%{boldEnd} вже налаштовано, і ви можете додати стільки правил, скільки вам потрібно, шляхом задавання специфікацій середовищ нижче. Ви можете перемикати стан для кожного з ваших правил, щоб зробити їх %{boldStart}Активними%{boldEnd} або %{boldStart}Неактивними%{boldEnd}."
msgid "FeatureFlags|Feature Flags"
-msgstr ""
+msgstr "Перемикачі функцій"
msgid "FeatureFlags|Feature Flags allow you to configure your code into different flavors by dynamically toggling certain functionality."
-msgstr ""
+msgstr "Перемикачі функцій дозволяють налаштовувати ваш код по-різному за допомогою динамічного увімкнення чи вимкнення певної функціональності."
msgid "FeatureFlags|Feature flag %{name} will be removed. Are you sure?"
-msgstr ""
+msgstr "Перемикач функції %{name} буде видалено. Ви впевнені?"
msgid "FeatureFlags|Get started with Feature Flags"
-msgstr ""
+msgstr "Розпочати роботу з перемикачами функцій"
msgid "FeatureFlags|Inactive"
msgstr "Неактивний"
msgid "FeatureFlags|Inactive flag for %{scope}"
-msgstr ""
+msgstr "Неактивний перемикач функції для %{scope}"
msgid "FeatureFlags|Install a %{docs_link_start}compatible client library%{docs_link_end} and specify the API URL, application name, and instance ID during the configuration setup."
msgstr "Встановіть %{docs_link_start}сумісну клієнтську бібліотеку%{docs_link_end} і вкажіть URL адресу API, назву застосунку та ідентифікатор інстанса під час налаштування."
@@ -4077,10 +4357,10 @@ msgid "FeatureFlags|Instance ID"
msgstr "Ідентифікатор Інстансу"
msgid "FeatureFlags|Loading Feature Flags"
-msgstr ""
+msgstr "Завантаження перемикачів функцій"
msgid "FeatureFlags|More Information"
-msgstr ""
+msgstr "Більше інформації"
msgid "FeatureFlags|More information"
msgstr "Більше інформації"
@@ -4094,26 +4374,23 @@ msgstr "Новий"
msgid "FeatureFlags|New Feature Flag"
msgstr "Новий перемикач функції"
-msgid "FeatureFlags|Save changes"
-msgstr "Зберегти зміни"
-
msgid "FeatureFlags|Status"
msgstr "Статус"
msgid "FeatureFlags|Target environments"
-msgstr ""
+msgstr "Цільові середовища"
msgid "FeatureFlags|There are no active Feature Flags"
-msgstr ""
+msgstr "Немає активних перемикачів функцій"
msgid "FeatureFlags|There are no inactive Feature Flags"
-msgstr ""
+msgstr "Немає неактивних перемикачів функцій"
msgid "FeatureFlags|There was an error fetching the feature flags."
-msgstr ""
+msgstr "Помилка при отриманні перемикачів функцій."
msgid "FeatureFlags|Try again in a few moments or contact your support team."
-msgstr ""
+msgstr "Повторіть спробу через деякий час або зверніться до вашої служби підтримки."
msgid "Feb"
msgstr "лют."
@@ -4126,31 +4403,31 @@ msgstr "Поля на цій сторінці зараз недоступні д
msgid "File"
msgid_plural "Files"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Файл"
+msgstr[1] "Файли"
+msgstr[2] "Файлів"
+msgstr[3] "Файлів"
msgid "File added"
-msgstr ""
+msgstr "Файл додано"
msgid "File browser"
-msgstr ""
+msgstr "Файловий менеджер"
msgid "File deleted"
-msgstr ""
+msgstr "Файл видалено"
msgid "File mode changed from %{a_mode} to %{b_mode}"
msgstr ""
msgid "File moved"
-msgstr ""
+msgstr "Файл переміщено"
msgid "File templates"
msgstr "Шаблони файлів"
msgid "File upload error."
-msgstr ""
+msgstr "Помилка завантаження файлу."
msgid "Files"
msgstr "Файли"
@@ -4168,31 +4445,28 @@ msgid "Filter by %{issuable_type} that are currently opened."
msgstr "Фільтрувати відкриті за %{issuable_type}."
msgid "Filter by commit message"
-msgstr "Фільтрувати за коміт-повідомленням"
+msgstr "Фільтрувати за повідомленням для коміту"
msgid "Filter by milestone name"
-msgstr ""
+msgstr "Фільтрувати за назвою етапу"
msgid "Filter by two-factor authentication"
-msgstr ""
+msgstr "Фільтрувати за двофакторною автентифікацією"
msgid "Filter results by group"
-msgstr ""
+msgstr "Фільтрувати результати за групою"
msgid "Filter results by project"
-msgstr ""
+msgstr "Фільтрувати результати за проектом"
msgid "Filter..."
msgstr "Фільтр..."
-msgid "Find and manage Auth Tokens in your Sentry account settings page."
-msgstr ""
-
msgid "Find by path"
msgstr "Пошук по шляху"
msgid "Find existing members by name"
-msgstr ""
+msgstr "Знайти існуючих учасників за ім'ям"
msgid "Find file"
msgstr "Знайти файл"
@@ -4203,11 +4477,14 @@ msgstr "Знайдіть завантажений ZIP-файл і розпаку
msgid "Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file."
msgstr "Знайдіть щойно розпакований <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> файл."
+msgid "Fingerprint"
+msgstr ""
+
msgid "Fingerprints"
msgstr "Відбитки пальців"
msgid "Finish editing this message first!"
-msgstr ""
+msgstr "Спочатку завершіть редагування цього повідомлення!"
msgid "Finish review"
msgstr "Завершити перевірку"
@@ -4216,7 +4493,7 @@ msgid "Finished"
msgstr "Завершено"
msgid "First day of the week"
-msgstr ""
+msgstr "Перший день тижня"
msgid "FirstPushedBy|First"
msgstr "Перший"
@@ -4264,7 +4541,7 @@ msgid "For internal projects, any logged in user can view pipelines and access j
msgstr "Для внутрішніх проектів будь-який зареєстрований користувач може переглядати конвеєри та отримати доступ до інформації про роботу (логи та артефакти)"
msgid "For more info, read the documentation."
-msgstr ""
+msgstr "Для отримання додаткової інформації читайте документацію."
msgid "For more information, go to the "
msgstr "Для отримання додаткової інформації, відвідайте "
@@ -4291,7 +4568,7 @@ msgid "Forking in progress"
msgstr "Відбувається створення форку"
msgid "Forks"
-msgstr ""
+msgstr "Форки"
msgid "Format"
msgstr "Формат"
@@ -4302,8 +4579,8 @@ msgstr "Знайдено помилки у вашому .gitlab-ci.yml:"
msgid "Free Trial of GitLab.com Gold"
msgstr "Безкоштовна пробна версія GitLab.com Gold"
-msgid "From %{provider_title}"
-msgstr "З %{provider_title}"
+msgid "From %{providerTitle}"
+msgstr ""
msgid "From Bitbucket"
msgstr "З Bitbucket"
@@ -4332,12 +4609,21 @@ msgstr "З етапів:"
msgid "From the Kubernetes cluster details view, install Runner from the applications list"
msgstr "Із сторінки деталей Kubernetes-кластера, встановіть runner зі списку застосунків"
+msgid "GPG Key ID:"
+msgstr ""
+
msgid "GPG Keys"
msgstr "GPG ключі"
+msgid "GPG signature (loading...)"
+msgstr ""
+
msgid "General"
msgstr "Загальні"
+msgid "General Settings"
+msgstr ""
+
msgid "General pipelines"
msgstr "Загальні конвеєри"
@@ -4345,7 +4631,7 @@ msgid "Generate a default set of labels"
msgstr "Створити стандартний набір міток"
msgid "Generate key"
-msgstr ""
+msgstr "Згенерувати ключ"
msgid "Geo"
msgstr "Geo"
@@ -4362,6 +4648,9 @@ msgstr "Вузол не працює або зламаний."
msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
msgstr "Вузол працює повільно, перевантажений або тільки що відновився після збою."
+msgid "GeoNodes|Alternate URL"
+msgstr ""
+
msgid "GeoNodes|Checksummed"
msgstr "Із контрольною сумою"
@@ -4426,7 +4715,7 @@ msgid "GeoNodes|New node"
msgstr "Новий вузол"
msgid "GeoNodes|Node Authentication was successfully repaired."
-msgstr "Аутентифікацію вузла успішно полагоджено."
+msgstr "Автентифікацію вузла успішно полагоджено."
msgid "GeoNodes|Node was successfully removed."
msgstr "Вузол успішно видалено."
@@ -4438,10 +4727,10 @@ msgid "GeoNodes|Out of sync"
msgstr "Розсинхронізовані"
msgid "GeoNodes|Removing a primary node stops the sync process for all nodes. Syncing cannot be resumed without losing some data on all secondaries. In this case we would recommend setting up all nodes from scratch. Are you sure?"
-msgstr ""
+msgstr "Видалення основного вузла зупиняє процес синхронізації для всіх вузлів. Синхронізацію неможливо буде відновити без втрати деяких даних на всіх вторинних вузлах. У цьому випадку рекомендовано налаштувати всі вузли з нуля. Ви впевнені?"
msgid "GeoNodes|Removing a secondary node stops the sync process. It is not currently possible to add back the same node without losing some data. We only recommend setting up a new secondary node in this case. Are you sure?"
-msgstr ""
+msgstr "Видалення вторинного вузла зупиняє процес синхронізації. Наразі неможливо додати назад той самий вузол без втрати деяких даних. У цьому випадку рекомендовано створити новий вторинний вузол. Ви впевнені?"
msgid "GeoNodes|Replication slot WAL"
msgstr "Слот реплікації WAL"
@@ -4516,7 +4805,7 @@ msgid "GeoNodes|Wikis verified with their counterparts on the Primary node"
msgstr "Вікі перевірено із їхніми копіями на первинному вузлі"
msgid "GeoNodes|With %{geo} you can install a special read-only and replicated instance anywhere. Before you add nodes, follow the %{instructions} in the exact order they appear."
-msgstr ""
+msgstr "З %{geo} ви можете встановити будь-де спеціальній реплікований інстанс лише для читання. Перед тим, як додавати вузли, дотримайтеся %{instructions} в точному порядку."
msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
msgstr "Ви налаштували Geo-вузли через незахищене HTTP-з’єднання. Ми рекомендуємо використовувати HTTPS."
@@ -4542,9 +4831,24 @@ msgstr "Всі проекти плануються для повторної п
msgid "Geo|All projects are being scheduled for re-sync"
msgstr "Всі проекти плануються для повторної синхронізації"
+msgid "Geo|Alternate URL"
+msgstr ""
+
msgid "Geo|Batch operations"
msgstr "Групові операції"
+msgid "Geo|Choose which groups you wish to synchronize to this secondary node."
+msgstr ""
+
+msgid "Geo|Control the maximum concurrency of LFS/attachment backfill for this secondary node"
+msgstr ""
+
+msgid "Geo|Control the maximum concurrency of verification operations for this Geo node"
+msgstr ""
+
+msgid "Geo|Control the minimum interval in days that a repository should be reverified for this primary node"
+msgstr ""
+
msgid "Geo|Could not remove tracking entry for an existing project."
msgstr "Не вдалося видалити запис відстеження для існуючого проекту."
@@ -4564,7 +4868,7 @@ msgid "Geo|In sync"
msgstr "Синхронізовано"
msgid "Geo|Last repository check run"
-msgstr ""
+msgstr "Остання запуск перевірки репозиторію"
msgid "Geo|Last successful sync"
msgstr "Остання успішна синхронізація"
@@ -4603,7 +4907,7 @@ msgid "Geo|Projects in certain storage shards"
msgstr "Проекти в певних сегментах сховищ"
msgid "Geo|Re-verification interval"
-msgstr ""
+msgstr "Інтервал повторної перевірки"
msgid "Geo|Recheck"
msgstr "Повторна перевірка"
@@ -4632,6 +4936,9 @@ msgstr "Кількість спроб"
msgid "Geo|Select groups to replicate."
msgstr "Виберіть групи для реплікації."
+msgid "Geo|Selective synchronization"
+msgstr ""
+
msgid "Geo|Shards to synchronize"
msgstr "Сегменти для синхронізації"
@@ -4644,12 +4951,21 @@ msgstr "Синхронізовано"
msgid "Geo|Synchronization failed - %{error}"
msgstr "Синхронізація невдала: %{error}"
+msgid "Geo|This is a primary node"
+msgstr ""
+
+msgid "Geo|To support OAuth logins to this node at a different domain than URL"
+msgstr ""
+
msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed."
msgstr "Запис відстеження для проекту (%{project_id}) успішно видалено."
msgid "Geo|Tracking entry will be removed. Are you sure?"
msgstr "Буде видалено запис про відстеження. Ви впевнені?"
+msgid "Geo|URL"
+msgstr ""
+
msgid "Geo|Unknown state"
msgstr "Невідомий стан"
@@ -4669,7 +4985,7 @@ msgid "Geo|You are on a secondary, <b>read-only</b> Geo node. You may be able to
msgstr "Ви знаходитесь на вторинному <b>лише для читання</b> Geo-вузлі. Ви зможете вносити лише обмежену кількість змін та виконувати обмежений набір операцій з цієї сторінки."
msgid "Geo|You need a different license to use Geo replication"
-msgstr "Вам потрібна інша ліцензія на використання географічної реплікації"
+msgstr "Вам потрібна інша ліцензія на використання Geo реплікації"
msgid "Geo|misconfigured"
msgstr "неправильно налаштований"
@@ -4684,16 +5000,16 @@ msgid "Get a free instance review"
msgstr "Отримайте безкоштовну оцінку інстанса"
msgid "Get started with error tracking"
-msgstr ""
+msgstr "Розпочати роботу з відстеженням помилок"
msgid "Getting started with releases"
-msgstr ""
+msgstr "Розпочати роботу з релізами"
msgid "Git"
msgstr "Git"
msgid "Git global setup"
-msgstr ""
+msgstr "Глобальні налаштування Git"
msgid "Git repository URL"
msgstr "URL Git-репозиторія"
@@ -4762,20 +5078,29 @@ msgid "Gitea Import"
msgstr "Імпорт з Gitea"
msgid "Given access %{time_ago}"
-msgstr ""
+msgstr "Надано доступ %{time_ago}"
msgid "Go Back"
msgstr "Повернутися"
+msgid "Go Micro is a framework for micro service development."
+msgstr ""
+
msgid "Go back"
msgstr "Повернутися"
+msgid "Go full screen"
+msgstr "На повний екран"
+
msgid "Go to"
msgstr "Перейти до"
msgid "Go to %{link_to_google_takeout}."
msgstr "Перейти до %{link_to_google_takeout}."
+msgid "Go to project"
+msgstr "Перейти до проекту"
+
msgid "Google Code import"
msgstr "Імпорт з Google Code"
@@ -4783,13 +5108,13 @@ msgid "Google Takeout"
msgstr "Google Takeout"
msgid "Google authentication is not %{link_to_documentation}. Ask your GitLab administrator if you want to use this service."
-msgstr "Аутентифікація Google не %{link_to_documentation}. Попросіть свого адміністратора GitLab, якщо ви хочете скористатися цим сервісом."
+msgstr "Автентифікація Google не %{link_to_documentation}. Попросіть свого адміністратора GitLab, якщо ви хочете скористатися цим сервісом."
msgid "Got it!"
msgstr "Зрозуміло!"
msgid "Grant access"
-msgstr ""
+msgstr "Надати доступ"
msgid "Graph"
msgstr "Графік"
@@ -4806,6 +5131,9 @@ msgstr "Статус групи Git LFS:"
msgid "Group ID"
msgstr "Ідентифікатор групи"
+msgid "Group ID: %{group_id}"
+msgstr ""
+
msgid "Group Runners"
msgstr "Групові Runner'и"
@@ -4833,14 +5161,17 @@ msgstr "Інформація про групу:"
msgid "Group maintainers can register group runners in the %{link}"
msgstr "Керівники групи можуть зареєструвати групові runner'и через %{link}"
+msgid "Group managed accounts"
+msgstr ""
+
msgid "Group name"
msgstr "Назва групи"
msgid "Group overview content"
-msgstr ""
+msgstr "Вміст оглядової сторінки групи"
msgid "Group:"
-msgstr ""
+msgstr "Група:"
msgid "Group: %{group_name}"
msgstr "Група: %{group_name}"
@@ -4858,33 +5189,48 @@ msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timelin
msgstr "План-графік епіків відображає стан ваших епіків у часі"
msgid "GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from %{startDate} to %{endDate}."
-msgstr ""
+msgstr "Для перегляду плану-графіку, додайте дату початку чи закінчення до одного з ваших епіків в цій групі або її підгрупах; від %{startDate} до %{endDate}."
msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}."
-msgstr ""
+msgstr "Щоб розширити пошук, змініть або видаліть фільтри; від %{startDate} до %{endDate}."
msgid "GroupRoadmap|Until %{dateWord}"
msgstr "До %{dateWord}"
+msgid "GroupSettings|Auto DevOps pipeline was updated for the group"
+msgstr ""
+
+msgid "GroupSettings|Auto DevOps will automatically build, test and deploy your application based on a predefined Continuous Integration and Delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end}"
+msgstr ""
+
msgid "GroupSettings|Badges"
msgstr "Значки"
msgid "GroupSettings|Custom project templates"
-msgstr ""
+msgstr "Власні шаблони проектів"
msgid "GroupSettings|Customize your group badges."
msgstr "Налаштувати значки групи."
+msgid "GroupSettings|Default to Auto DevOps pipeline for all projects within this group"
+msgstr ""
+
msgid "GroupSettings|Learn more about badges."
msgstr "Дізнайтеся більше про значки."
msgid "GroupSettings|Learn more about group-level project templates."
-msgstr ""
+msgstr "Докладніше про шаблони проектів на рівні групи."
msgid "GroupSettings|Prevent sharing a project within %{group} with other groups"
msgstr "Заборонити спільний доступ до проекту в рамках %{group} з іншими групами"
msgid "GroupSettings|Select a sub-group as the custom project template source for this group."
+msgstr "Виберіть підгрупу як джерело власних шаблонів проектів для цієї групи."
+
+msgid "GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found."
+msgstr ""
+
+msgid "GroupSettings|There was a problem updating Auto DevOps pipeline: %{error_messages}."
msgstr ""
msgid "GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup."
@@ -4912,7 +5258,7 @@ msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroup
msgstr "Групи також можуть бути вкладеними при використанні %{subgroup_docs_link_start}підгруп%{subgroup_docs_link_end}."
msgid "Groups with access to <strong>%{project_name}</strong>"
-msgstr ""
+msgstr "Групи з доступом до <strong>%{project_name}</strong>"
msgid "GroupsDropdown|Frequently visited"
msgstr "Часто відвідувані"
@@ -5014,7 +5360,7 @@ msgid "Here is the public SSH key that needs to be added to the remote server. F
msgstr "Це відкритий (публічний) SSH ключ, який потрібно додати на віддалений сервер. Для отримання додаткової інформації, зверніться до документації."
msgid "Hide file browser"
-msgstr ""
+msgstr "Сховати файловий менеджер"
msgid "Hide host keys manual input"
msgstr "Сховати ввід ключів хоста"
@@ -5030,7 +5376,7 @@ msgstr[2] "Сховати значень"
msgstr[3] "Сховати значень"
msgid "Hide values"
-msgstr ""
+msgstr "Сховати значення"
msgid "History"
msgstr "Історія"
@@ -5038,9 +5384,12 @@ 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."
+msgid "Housekeeping, export, path, transfer, remove, archive."
msgstr ""
+msgid "However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation."
+msgstr "Проте ви вже є учасником цього %{member_source}. Увійдіть, використовуючи інший обліковий запис, щоб прийняти запрошення."
+
msgid "I accept the %{terms_link}"
msgstr "Я приймаю %{terms_link}"
@@ -5120,7 +5469,7 @@ msgid "If you already have files you can push them using the %{link_to_cli} belo
msgstr "Якщо у вас уже є файли, ви можете відправити їх за допомогою %{link_to_cli} нижче."
msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
-msgstr "Якщо ваш HTTP-репозиторій не є публічним, додайте дані для аутентифікації до URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
+msgstr "Якщо ваш HTTP-репозиторій не є публічним, додайте дані для автентифікації до URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
msgid "ImageDiffViewer|2-up"
msgstr "2 поруч"
@@ -5132,13 +5481,13 @@ msgid "ImageDiffViewer|Swipe"
msgstr "Накладені (проведення)"
msgid "Impersonation has been disabled"
-msgstr ""
+msgstr "Уособлення було вимкнено"
msgid "Import"
msgstr "Імпорт"
msgid "Import CSV"
-msgstr ""
+msgstr "Імпортувати CSV"
msgid "Import Projects from Gitea"
msgstr "Імпортувати проекти з Gitea"
@@ -5159,13 +5508,13 @@ msgid "Import in progress"
msgstr "Імпорт триває"
msgid "Import issues"
-msgstr ""
+msgstr "Імпорт задач"
msgid "Import members"
-msgstr ""
+msgstr "Імпортувати учасників"
msgid "Import members from another project"
-msgstr ""
+msgstr "Імпортувати учасників з іншого проекту"
msgid "Import multiple repositories by uploading a manifest file."
msgstr "Імпортувати кілька репозиторіїв, надіславши файл маніфесту."
@@ -5174,7 +5523,7 @@ msgid "Import project"
msgstr "Імпорт проекту"
msgid "Import project members"
-msgstr ""
+msgstr "Імпортувати учасників проекту"
msgid "Import projects from Bitbucket"
msgstr "Імпортувати проекти з Bitbucket"
@@ -5203,9 +5552,24 @@ msgstr "Імпорт репозиторію"
msgid "Import timed out. Import took longer than %{import_jobs_expiration} seconds"
msgstr ""
+msgid "Import/Export illustration"
+msgstr ""
+
msgid "ImportButtons|Connect repositories from"
msgstr "Підключити репозиторії із"
+msgid "ImportProjects|Importing the project failed"
+msgstr ""
+
+msgid "ImportProjects|Requesting your %{provider} repositories failed"
+msgstr ""
+
+msgid "ImportProjects|Select the projects you want to import"
+msgstr ""
+
+msgid "ImportProjects|Updating the imported projects failed"
+msgstr ""
+
msgid "Improve Issue boards with GitLab Enterprise Edition."
msgstr "Покращити дошки обговорень за допомогою версії GitLab Enterprise Edition."
@@ -5228,19 +5592,19 @@ msgid "Include a Terms of Service agreement and Privacy Policy that all users mu
msgstr "Включити угоду про надання послуг та правила конфіденційності, які повинні прийняти всі користувачі."
msgid "Include merge request description"
-msgstr ""
+msgstr "Додайте опис запиту на злиття"
msgid "Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>."
msgstr "Якщо необхідно додайте ім'я користувача в URL: <code>https: //username@gitlab.company.com/group/project.git</code>."
msgid "Includes an MVC structure to help you get started."
-msgstr ""
+msgstr "Включає структуру MVC, щоб допомогти вам розпочати роботу."
msgid "Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started."
-msgstr ""
+msgstr "Включає структуру MVC, Gemfile, Rakefile, а також багато іншого, щоб допомогти вам розпочати роботу."
msgid "Includes an MVC structure, mvnw and pom.xml to help you get started."
-msgstr ""
+msgstr "Включає структуру MVC, mvnw і pom.xml, щоб допомогти вам розпочати роботу."
msgid "Incompatible Project"
msgstr "Несумісний проект"
@@ -5257,7 +5621,16 @@ msgstr "Введіть ключі хоста вручну"
msgid "Input your repository URL"
msgstr "Введіть ваш URL репозиторію"
+msgid "Insert a quote"
+msgstr "Вставити цитату"
+
+msgid "Insert code"
+msgstr "Вставити код"
+
msgid "Insert suggestion"
+msgstr "Додати пропозицію"
+
+msgid "Insights"
msgstr ""
msgid "Install GitLab Runner"
@@ -5292,7 +5665,7 @@ msgid "Interested parties can even contribute by pushing commits if they want to
msgstr "Зацікавлені сторони за бажанням можуть навіть робити внески шляхом відправлення комітів."
msgid "Internal"
-msgstr ""
+msgstr "Внутрішній"
msgid "Internal - The group and any internal projects can be viewed by any logged in user."
msgstr "Внутрішня — будь-який автентифікований користувач має доступ до цієї групи та усіх її внутрішніх проектів."
@@ -5312,8 +5685,11 @@ msgstr "Представляємо аналітику циклу"
msgid "Introducing Your Conversational Development Index"
msgstr ""
+msgid "Invalid input, please avoid emojis"
+msgstr "Некорректний ввід, будь ласка, уникайте смайликів"
+
msgid "Invitation"
-msgstr ""
+msgstr "Запрошення"
msgid "Invite"
msgstr "Запрошення"
@@ -5322,7 +5698,7 @@ msgid "Invite group"
msgstr ""
msgid "Invite member"
-msgstr ""
+msgstr "Запросити учасника"
msgid "Invoke Count"
msgstr ""
@@ -5330,6 +5706,9 @@ msgstr ""
msgid "Invoke Time"
msgstr ""
+msgid "IssuableStatus|Closed (%{moved_link_start}moved%{moved_link_end})"
+msgstr ""
+
msgid "Issue"
msgstr "Задача"
@@ -5342,6 +5721,9 @@ msgstr "Режим фокусування для дошки задач"
msgid "Issue events"
msgstr "Задачі"
+msgid "Issue settings"
+msgstr ""
+
msgid "IssueBoards|Board"
msgstr "Дошка"
@@ -5349,19 +5731,19 @@ msgid "IssueBoards|Boards"
msgstr "Дошки"
msgid "IssueBoards|Create new board"
-msgstr ""
+msgstr "Створити нову дошку"
msgid "IssueBoards|Delete board"
-msgstr ""
+msgstr "Видалити дошку"
msgid "IssueBoards|No matching boards found"
-msgstr ""
+msgstr "Не знайдено відповідних дошок"
msgid "IssueBoards|Some of your boards are hidden, activate a license to see them again."
-msgstr ""
+msgstr "Деякі з ваших дошок є прихованими, активуйте ліцензію, щоб побачити їх знову."
msgid "IssueBoards|Switch board"
-msgstr ""
+msgstr "Перемкнути дошку"
msgid "Issues"
msgstr "Задачі"
@@ -5372,8 +5754,8 @@ msgstr "Задачі можуть бути помилками, нагадува
msgid "Issues closed"
msgstr "Задачі закриті"
-msgid "Issues, merge requests, pushes and comments."
-msgstr "Задачі, запити на злиття, відправлення коду і коментарі."
+msgid "Issues, merge requests, pushes, and comments."
+msgstr ""
msgid "IssuesAnalytics|After you begin creating issues for your projects, we can start tracking and displaying metrics for them"
msgstr "Після створення задач для ваших проектів, ми зможемо почати відстежувати і відображати метрики для них"
@@ -5400,7 +5782,7 @@ msgid "It must have a header row and at least two columns: the first column is t
msgstr ""
msgid "It's you"
-msgstr ""
+msgstr "Це ви"
msgid "Jaeger URL"
msgstr "URL-адреса Jaeger"
@@ -5421,10 +5803,10 @@ msgid "Job has been erased"
msgstr "Завдання було стерте"
msgid "Job is stuck. Check runners."
-msgstr ""
+msgstr "Завдання заблоковане. Перевірте runner'и."
msgid "Job was retried"
-msgstr ""
+msgstr "Завдання було перезапущене"
msgid "Jobs"
msgstr "Завдання"
@@ -5466,10 +5848,10 @@ msgid "Job|The artifacts were removed"
msgstr "Артефакти були видалені"
msgid "Job|The artifacts will be removed"
-msgstr ""
+msgstr "Артефакти будуть видалені"
msgid "Job|This job is stuck because the project doesn't have any runners online assigned to it."
-msgstr ""
+msgstr "Це завдання заблоковане, тому що цей проект не має жодних runner'ів призначених для нього."
msgid "Jul"
msgstr "лип."
@@ -5484,7 +5866,7 @@ msgid "June"
msgstr "червень"
msgid "Key (PEM)"
-msgstr ""
+msgstr "Ключ (PEM)"
msgid "Kubernetes"
msgstr "Kubernetes"
@@ -5493,7 +5875,7 @@ msgid "Kubernetes Cluster"
msgstr "Кластер Kubernetes"
msgid "Kubernetes Clusters"
-msgstr ""
+msgstr "Кластери Kubernetes"
msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}"
msgstr "Перевищення ліміту часу при створенні Kubernetes-кластера; %{timeout}"
@@ -5561,6 +5943,9 @@ msgstr "Перенести мітку"
msgid "Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. Existing project labels with the same title will be merged. This action cannot be reversed."
msgstr "Перенесення %{labelTitle} на рівень групи зробить її доступною для всіх проектів в групі %{groupName}. Існуючі проектні мітки із такими ж іменами будуть об'єднані. Дана дія не може бути скасована."
+msgid "Language"
+msgstr "Мова"
+
msgid "Large File Storage"
msgstr "Сховище великих файлів (LFS)"
@@ -5575,7 +5960,7 @@ msgid "Last Pipeline"
msgstr "Останній Конвеєр"
msgid "Last activity"
-msgstr ""
+msgstr "Остання активність"
msgid "Last commit"
msgstr "Останній коміт"
@@ -5611,7 +5996,7 @@ msgid "Latest changes"
msgstr "Останні зміни"
msgid "Latest pipeline for this branch"
-msgstr ""
+msgstr "Останній конвеєр для цієї гілки"
msgid "Lead"
msgstr ""
@@ -5626,26 +6011,32 @@ msgid "Learn more about %{issue_boards_url}, to keep track of issues in multiple
msgstr "Дізнайтеся більше про %{issue_boards_url}, щоб стежити за задачами в кількох списках, використовуючи мітки, виконавців та етапи. Якщо вам чогось не вистачає в дошках обговорень задач, створіть задачу на %{gitlab_issues_url}."
msgid "Learn more about Auto DevOps"
-msgstr ""
+msgstr "Дізнайтеся більше про Auto DevOps"
msgid "Learn more about Kubernetes"
msgstr "Дізнайтеся більше про Kubernetes"
msgid "Learn more about Web Terminal"
+msgstr "Дізнатися більше про Веб-термінал"
+
+msgid "Learn more about approvals."
msgstr ""
msgid "Learn more about custom project templates"
-msgstr ""
+msgstr "Докладніше про власні шаблони проектів"
msgid "Learn more about group-level project templates"
-msgstr ""
+msgstr "Докладніше про шаблони проектів на рівні групи"
msgid "Learn more about incoming email addresses"
-msgstr ""
+msgstr "Докладніше про вхідні адреси електронної пошти"
msgid "Learn more about protected branches"
msgstr "Дізнайтеся більше про захищені гілки"
+msgid "Learn more about signing commits"
+msgstr ""
+
msgid "Learn more in the"
msgstr "Дізнайтесь більше"
@@ -5783,10 +6174,10 @@ msgid "Loading..."
msgstr "Завантаження..."
msgid "Loading…"
-msgstr ""
+msgstr "Завантаження…"
msgid "Localization"
-msgstr ""
+msgstr "Регіональні налаштування"
msgid "Lock"
msgstr "Блокувати"
@@ -5821,6 +6212,21 @@ msgstr "Вхід за допомогою смарт-картки"
msgid "Logs"
msgstr "Логи"
+msgid "MRApprovals|Approved by"
+msgstr ""
+
+msgid "MRApprovals|Approvers"
+msgstr "Затверджуючі особи"
+
+msgid "MRApprovals|Pending approvals"
+msgstr ""
+
+msgid "MRDiff|Show changes only"
+msgstr ""
+
+msgid "MRDiff|Show full file"
+msgstr ""
+
msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
msgstr "Зробіть кожного учасника команди більш продуктивним незалежно від його місцезнаходження. GitLab Geo створює копії \"тільки для читання\" вашого GitLab сервера, щоб скоротити час для клонування і отримання коду з великих репозиторіїв."
@@ -5855,7 +6261,7 @@ msgid "Manage project labels"
msgstr "Керування мітками проекту"
msgid "Manage two-factor authentication"
-msgstr ""
+msgstr "Керування двофакторною автентифікацією"
msgid "Manage your group’s membership while adding another level of security with SAML."
msgstr "Керуйте членством у вашій групі додаючи ще один рівень безпеки із SAML."
@@ -5866,6 +6272,9 @@ msgstr "Маніфест"
msgid "Manifest file import"
msgstr "Імпортувати файл маніфесту"
+msgid "Manual job"
+msgstr ""
+
msgid "Map a FogBugz account ID to a GitLab user"
msgstr "Зв’язати обліковий запис FogBugz з користувачем GitLab"
@@ -5888,41 +6297,11 @@ msgid "Mark todo as done"
msgstr "Відмітити завдання виконаним"
msgid "Markdown"
-msgstr ""
+msgstr "Markdown"
msgid "Markdown enabled"
msgstr "Markdown увімкнено"
-msgid "MarkdownToolbar|Add a bullet list"
-msgstr "Додати ненумерований список"
-
-msgid "MarkdownToolbar|Add a link"
-msgstr "Додати посилання"
-
-msgid "MarkdownToolbar|Add a numbered list"
-msgstr "Додати нумерований список"
-
-msgid "MarkdownToolbar|Add a table"
-msgstr "Додати таблицю"
-
-msgid "MarkdownToolbar|Add a task list"
-msgstr "Додати список завдань"
-
-msgid "MarkdownToolbar|Add bold text"
-msgstr "Додати жирний текст"
-
-msgid "MarkdownToolbar|Add italic text"
-msgstr "Додати курсивний текст"
-
-msgid "MarkdownToolbar|Go full screen"
-msgstr "Повний екран"
-
-msgid "MarkdownToolbar|Insert a quote"
-msgstr "Вставити цитату"
-
-msgid "MarkdownToolbar|Insert code"
-msgstr "Вставити код"
-
msgid "Maven Metadata"
msgstr "Maven-метадані"
@@ -5948,10 +6327,10 @@ msgid "Members"
msgstr "Користувачі"
msgid "Members can be added by project <i>Maintainers</i> or <i>Owners</i>"
-msgstr ""
+msgstr "Учасники можуть будуть додані <i>Керівниками</i> або <i>Власниками</i> проекту"
msgid "Members of <strong>%{project_name}</strong>"
-msgstr ""
+msgstr "Учасники <strong>%{project_name}</strong>"
msgid "Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
msgstr "Учасники будуть перенаправлені сюди, коли будуть заходити до вашої групи. Отримайте його від свого провайдера ідентифікації, де вона також може називатися \"SSO Service Location\", \"SAML Token Issuance Endpoint\", або \"SAML 2.0/W-Federation URL\"."
@@ -5966,15 +6345,18 @@ msgid "Merge Requests created"
msgstr "Запит на злиття було створено"
msgid "Merge commit message"
-msgstr ""
+msgstr "Повідомлення для коміту-злиття"
msgid "Merge events"
msgstr "Події злиття"
msgid "Merge immediately"
-msgstr ""
+msgstr "Злити негайно"
msgid "Merge in progress"
+msgstr "Злиття в процесі"
+
+msgid "Merge pipelines will try to validate the post-merge result prior to merging"
msgstr ""
msgid "Merge request"
@@ -5990,31 +6372,31 @@ msgid "Merge requests are a place to propose changes you've made to a project an
msgstr "Запит на злиття — це спосіб запропонувати свої зміни до проекту і обговорити їх із іншими"
msgid "Merge when pipeline succeeds"
-msgstr ""
+msgstr "Злити, коли конвеєр успішно завершиться"
msgid "MergeRequests|Add a reply"
-msgstr ""
+msgstr "Додати відповідь"
msgid "MergeRequests|An error occurred while saving the draft comment."
msgstr "Виникла помилка під час збереження чернетки коментаря."
msgid "MergeRequests|Discussion stays resolved"
-msgstr ""
+msgstr "Обговорення залишається вирішеним"
msgid "MergeRequests|Discussion stays unresolved"
-msgstr ""
+msgstr "Обговорення залишається невирішеним"
msgid "MergeRequests|Discussion will be resolved"
-msgstr ""
+msgstr "Обговорення буде вирішеним"
msgid "MergeRequests|Discussion will be unresolved"
-msgstr ""
+msgstr "Обговорення буде невирішеним"
msgid "MergeRequests|Jump to next unresolved discussion"
-msgstr ""
+msgstr "Перейти до наступного невирішеного обговорення"
msgid "MergeRequests|Reply..."
-msgstr ""
+msgstr "Відповісти..."
msgid "MergeRequests|Resolve this discussion in a new issue"
msgstr "Вирішити це обговорення в новій задачі"
@@ -6032,26 +6414,29 @@ msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr "Переглянути замінений файл станом на %{commitId}"
msgid "MergeRequests|commented on commit %{commitLink}"
-msgstr ""
+msgstr "прокоментував (-ла) коміт %{commitLink}"
msgid "MergeRequests|started a discussion"
-msgstr ""
+msgstr "розпочав (-ла) обговорення"
msgid "MergeRequests|started a discussion on %{linkStart}an old version of the diff%{linkEnd}"
-msgstr ""
+msgstr "розпочав (-ла) обговорення %{linkStart}старої версії порівняння (diff)%{linkEnd}"
msgid "MergeRequests|started a discussion on %{linkStart}the diff%{linkEnd}"
-msgstr ""
+msgstr "розпочав (-ла) обговорення %{linkStart}порівняння (diff)%{linkEnd}"
msgid "MergeRequests|started a discussion on an outdated change in commit %{linkStart}%{commitId}%{linkEnd}"
-msgstr ""
+msgstr "розпочав (-ла) обговорення застарілих змін в коміті %{linkStart}%{commitId}%{linkEnd}"
msgid "MergeRequests|started a discussion on commit %{linkStart}%{commitId}%{linkEnd}"
-msgstr ""
+msgstr "розпочав (-ла) коміту %{linkStart}%{commitId}%{linkEnd}"
msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
msgstr "%{paragraphStart} опис змінено %{descriptionChangedTimes} раз(а,ів) %{timeDifferenceMinutes}%{paragraphEnd}"
+msgid "MergeRequest|Error loading full diff. Please try again."
+msgstr ""
+
msgid "MergeRequest|Filter files"
msgstr "Фільтр файлів"
@@ -6059,7 +6444,7 @@ msgid "MergeRequest|No files found"
msgstr "Файлів не знайдено"
msgid "MergeRequest|Search files"
-msgstr ""
+msgstr "Пошук файлів"
msgid "Merged"
msgstr "Злито"
@@ -6080,7 +6465,7 @@ msgid "Metrics and profiling"
msgstr "Метрики та профілювання"
msgid "Metrics for environment"
-msgstr ""
+msgstr "Метрики для середовища"
msgid "Metrics|Check out the CI/CD documentation on deploying to an environment"
msgstr "Перевірте документацію CI/CD щодо розгортання в середовищі"
@@ -6089,10 +6474,10 @@ msgid "Metrics|Create metric"
msgstr "Створити метрику"
msgid "Metrics|Delete metric"
-msgstr ""
+msgstr "Видалити метрику"
msgid "Metrics|Delete metric?"
-msgstr ""
+msgstr "Видалити метрику?"
msgid "Metrics|Edit metric"
msgstr "Редагувати метрику"
@@ -6104,7 +6489,7 @@ msgid "Metrics|For grouping similar metrics"
msgstr "Для групування подібних метрик"
msgid "Metrics|Label of the y-axis (usually the unit). The x-axis always represents time."
-msgstr ""
+msgstr "Мітка осі - y (зазвичай одиниця вимірювання). Осі - x завжди позначає час."
msgid "Metrics|Learn about environments"
msgstr "Дізнайтеся більше про середовища"
@@ -6122,14 +6507,11 @@ msgid "Metrics|No deployed environments"
msgstr "Немає розгорнутих середовищ"
msgid "Metrics|PromQL query is valid"
-msgstr ""
+msgstr "Запит PromQL є дійсним"
msgid "Metrics|Prometheus Query Documentation"
msgstr "Документація по запитам Prometheus"
-msgid "Metrics|System"
-msgstr "Система"
-
msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr "Трапилася помилка під час отримання інформації про середовища. Будь ласка, спробуйте ще раз"
@@ -6140,7 +6522,7 @@ msgid "Metrics|There was an error getting environments information."
msgstr "Трапилася помилка під час отримання інформації про середовища."
msgid "Metrics|There was an error trying to validate your query"
-msgstr ""
+msgstr "Помилка при перевірці вашого запиту"
msgid "Metrics|There was an error while retrieving metrics"
msgstr "Трапилася помилка під час отримання метрик"
@@ -6164,7 +6546,7 @@ msgid "Metrics|Y-axis label"
msgstr "Назва осі Y"
msgid "Metrics|You're about to permanently delete this metric. This cannot be undone."
-msgstr ""
+msgstr "Ви збираєтеся остаточно видалити цю метрику. Це не можна відмінити."
msgid "Metrics|e.g. Throughput"
msgstr "напр. пропускна здатність"
@@ -6241,17 +6623,20 @@ msgstr "Скасувати"
msgid "Modal|Close"
msgstr "Закрити"
+msgid "Modify commit message"
+msgstr "Змінити повідомлення для комітів"
+
msgid "Modify commit messages"
-msgstr ""
+msgstr "Змінити повідомлення для комітів"
msgid "Modify merge commit"
-msgstr ""
+msgstr "Змінити коміт-злиття"
msgid "Monday"
-msgstr ""
+msgstr "Понеділок"
msgid "Monitor your errors by integrating with Sentry"
-msgstr ""
+msgstr "Моніторинг ваших помилок шляхом інтеграції із Sentry"
msgid "Monitoring"
msgstr "Моніторинг"
@@ -6274,6 +6659,9 @@ msgstr "Детальніше"
msgid "More information is available|here"
msgstr "тут"
+msgid "More than %{number_commits_distance} commits different with %{default_branch}"
+msgstr ""
+
msgid "Most stars"
msgstr "Найбільше в обраних"
@@ -6298,6 +6686,9 @@ msgstr "Назвіть ваш індивідуальний ключ за доп
msgid "Name:"
msgstr "Ім’я:"
+msgid "Naming, tags, avatar"
+msgstr ""
+
msgid "Naming, visibility"
msgstr "Найменування, видимість"
@@ -6314,7 +6705,7 @@ msgid "Nav|Sign out and sign in with a different account"
msgstr "Вийти і зайти під іншим обліковим записом"
msgid "Need help?"
-msgstr ""
+msgstr "Потрібна допомога?"
msgid "Network"
msgstr "Мережа"
@@ -6329,7 +6720,7 @@ msgid "New Application"
msgstr "Новий додаток"
msgid "New Environment"
-msgstr ""
+msgstr "Нове середовище"
msgid "New Group"
msgstr "Нова група"
@@ -6348,9 +6739,12 @@ msgid "New Label"
msgstr "Нова мітка"
msgid "New Milestone"
-msgstr ""
+msgstr "Новий етап"
msgid "New Pages Domain"
+msgstr "Новий домен Pages"
+
+msgid "New Password"
msgstr ""
msgid "New Pipeline Schedule"
@@ -6359,20 +6753,20 @@ msgstr "Новий розклад Конвеєра"
msgid "New Snippet"
msgstr "Новий сніпет"
-msgid "New Snippets"
-msgstr "Нові сніпети"
-
msgid "New branch"
msgstr "Нова гілка"
msgid "New branch unavailable"
msgstr "Нова гілка недоступна"
+msgid "New deploy key"
+msgstr ""
+
msgid "New directory"
msgstr "Новий каталог"
msgid "New environment"
-msgstr ""
+msgstr "Нове середовище"
msgid "New epic"
msgstr "Новий епік"
@@ -6396,7 +6790,7 @@ msgid "New merge request"
msgstr "Новий запит на злиття"
msgid "New milestone"
-msgstr ""
+msgstr "Новий етап"
msgid "New pipelines will cancel older, pending pipelines on the same branch"
msgstr "Нові конвеєри скасують старі, що очікують на тій же гілці"
@@ -6422,12 +6816,18 @@ msgstr "Новий..."
msgid "No"
msgstr "Ні"
+msgid "No %{providerTitle} repositories available to import"
+msgstr "Немає репозиторіїв %{providerTitle} для імпорту"
+
msgid "No Label"
msgstr "Без Мітки"
-msgid "No activities found"
+msgid "No Tag"
msgstr ""
+msgid "No activities found"
+msgstr "Не знайднено активностей"
+
msgid "No assignee"
msgstr "Немає виконавця"
@@ -6438,7 +6838,7 @@ msgid "No changes"
msgstr "Немає змін"
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
-msgstr ""
+msgstr "Немає змін між %{ref_start}%{source_branch}%{ref_end} та %{ref_start}%{target_branch}%{ref_end}"
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "Неможливо з'єднатись із сервером Gitaly, будь ласка, перевірте логи!"
@@ -6452,13 +6852,16 @@ msgstr "Внески не знайдено"
msgid "No credit card required."
msgstr "Не потрібна кредитна картка."
-msgid "No details available"
+msgid "No designs found."
msgstr ""
+msgid "No details available"
+msgstr "Немає деталей для відображення"
+
msgid "No due date"
msgstr "Немає"
-msgid "No errors to display"
+msgid "No errors to display."
msgstr ""
msgid "No estimate or time spent"
@@ -6468,7 +6871,7 @@ msgid "No file chosen"
msgstr "Файл не вибрано"
msgid "No file selected"
-msgstr ""
+msgstr "Файл не вибраний"
msgid "No files found."
msgstr "Не знайдено жодного файлу."
@@ -6483,7 +6886,7 @@ msgid "No license. All rights reserved"
msgstr "Немає ліцензії. Всі права захищені"
msgid "No matching results"
-msgstr ""
+msgstr "Немає відповідних результатів"
msgid "No merge requests for the selected time period."
msgstr "Немає запитів на злиття за вибраний період часу."
@@ -6495,13 +6898,13 @@ msgid "No messages were logged"
msgstr "Немає повідомлень у журналі"
msgid "No milestones to show"
-msgstr ""
+msgstr "Немає етапів для показу"
msgid "No other labels with such name or description"
msgstr "Немає інших міток з таким іменем або описом"
msgid "No preview for this file type"
-msgstr ""
+msgstr "Попереднього перегляду для цього типу файлів немає"
msgid "No prioritised labels with such name or description"
msgstr "Немає пріоритетних міток з таким іменем або описом"
@@ -6522,7 +6925,7 @@ msgid "No schedules"
msgstr "Немає розкладів"
msgid "No start date"
-msgstr ""
+msgstr "Немає дати початку"
msgid "No, directly import the existing email addresses and usernames."
msgstr "Ні, безпосередньо імпортувати існуючі адреси електронної пошти та імена користувачів."
@@ -6554,11 +6957,14 @@ msgstr "Недостатньо даних"
msgid "Not now"
msgstr "Пізніше"
+msgid "Not started"
+msgstr ""
+
msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
msgstr "Майте на увазі, що гілка master захищена автоматично. %{link_to_protected_branches}"
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 ""
+msgstr "Зауважте, що це запрошення було надіслано на %{mail_to_invite_email}, але ви увійшли як %{link_to_current_user} з електронною поштою %{mail_to_current_user}."
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
msgstr "Примітка: як адміністратор ви можете налаштувати %{github_integration_link}, що дозволить входити через GitHub і підключати репозиторії без створення особистого токену доступу."
@@ -6591,10 +6997,10 @@ msgid "Notification events"
msgstr "Повідомлення про події"
msgid "Notification setting"
-msgstr ""
+msgstr "Налаштування сповіщень"
msgid "Notification setting - %{notification_title}"
-msgstr ""
+msgstr "Параметр сповіщення - %{notification_title}"
msgid "NotificationEvent|Close issue"
msgstr "Задача закрита"
@@ -6703,13 +7109,13 @@ msgid "Only policy:"
msgstr ""
msgid "Only proceed if you trust %{idp_url} to control your GitLab account sign in."
-msgstr ""
+msgstr "Продовжуйте тільки якщо ви довіряєте %{idp_url} контроль над входом до вашого облікового запису GitLab."
msgid "Only project members can comment."
msgstr "Тільки учасники проекту можуть залишати коментарі."
msgid "Only project members will be imported. Group members will be skipped."
-msgstr ""
+msgstr "Лише учасника проекту будуть імпортовані. Учасники групи будуть пропущені."
msgid "Oops, are you sure?"
msgstr "Ой, а ви впевнені?"
@@ -6718,13 +7124,13 @@ msgid "Open"
msgstr "Відкриті"
msgid "Open Documentation"
-msgstr ""
+msgstr "Відкрити документацію"
msgid "Open comment type dropdown"
-msgstr ""
+msgstr "Випадаючий список типу коментарів"
msgid "Open errors"
-msgstr ""
+msgstr "Відкрити помилки"
msgid "Open in Xcode"
msgstr "Відкрити в Xcode"
@@ -6759,6 +7165,9 @@ msgstr "Операції"
msgid "Operations Dashboard"
msgstr "Панель керування операціями"
+msgid "Operations Settings"
+msgstr ""
+
msgid "OperationsDashboard|Add a project to the dashboard"
msgstr "Додайте проект до панелі керування"
@@ -6766,6 +7175,9 @@ msgid "OperationsDashboard|The operations dashboard provides a summary of each p
msgstr "Панель керування операціями містить інформацію про стан кожного з проектів разом зі станом його конвеєрів та попереджень."
msgid "OperationsDashboard|Unable to add %{invalidProjects}. The Operations Dashboard is available for public projects, and private projects in groups with a Gold plan."
+msgstr "Неможливо додати %{invalidProjects}. Панель керування операціями доступна для публічних та приватних проектів в групах із тарифним планом Gold."
+
+msgid "Optional"
msgstr ""
msgid "Optionally, you can %{link_to_customize} how FogBugz email addresses and usernames are imported into GitLab."
@@ -6814,10 +7226,10 @@ msgid "Pages"
msgstr "Сторінки"
msgid "Pages Domain"
-msgstr ""
+msgstr "Домен Pages"
msgid "Pages Domains"
-msgstr ""
+msgstr "Домени Pages"
msgid "Pagination|Last »"
msgstr "Остання »"
@@ -6832,10 +7244,7 @@ msgid "Pagination|« First"
msgstr "« Перша"
msgid "Parameter"
-msgstr ""
-
-msgid "Parent epic"
-msgstr ""
+msgstr "Параметр"
msgid "Part of merge request changes"
msgstr "Частина змін у запиті на злиття"
@@ -6844,13 +7253,16 @@ msgid "Password"
msgstr "Пароль"
msgid "Past due"
+msgstr "Прострочені"
+
+msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
msgid "Paste epic link"
-msgstr ""
+msgstr "Вставити посилання на епік"
msgid "Paste issue link"
-msgstr ""
+msgstr "Вставити посилання на задачу"
msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key."
msgstr "Вставте свій відкритий ключ SSH, який зазвичай знаходиться у файлі '~/.ssh/id_rsa.pub' і починається з 'ssh-rsa'. Не використовуйте свій приватний ключ SSH."
@@ -6889,17 +7301,14 @@ msgid "Personal Access Token"
msgstr "Токену персонального доступу"
msgid "Personal project creation is not allowed. Please contact your administrator with questions"
-msgstr ""
+msgstr "Створення персональних проектів не дозволено. Будь ласка, зверніться до свого адміністратор із питаннями"
msgid "Pick a name"
-msgstr ""
+msgstr "Виберіть ім'я"
msgid "Pipeline"
msgstr "Конвеєр"
-msgid "Pipeline Health"
-msgstr "Стан Конвеєра"
-
msgid "Pipeline Schedule"
msgstr "Розклад Конвеєра"
@@ -6960,11 +7369,17 @@ msgstr "Змінні"
msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Спеціальні"
+msgid "PipelineStatusTooltip|Commit: %{ci_status}"
+msgstr "Коміт: %{ci_status}"
+
+msgid "PipelineStatusTooltip|Pipeline: %{ci_status}"
+msgstr "Конвеєр: %{ci_status}"
+
msgid "Pipelines"
msgstr "Конвеєри"
msgid "Pipelines charts"
-msgstr "Чарти Конвеєрів"
+msgstr "Статистика конвеєрів"
msgid "Pipelines for last month"
msgstr "Конвеєри за останній місяць"
@@ -6975,6 +7390,9 @@ msgstr "Конвеєри за останній тиждень"
msgid "Pipelines for last year"
msgstr "Конвеєри за останній рік"
+msgid "Pipelines need to be configured to enable this feature."
+msgstr ""
+
msgid "Pipelines|Build with confidence"
msgstr "Виконуйте збірки із впевненістю"
@@ -7081,7 +7499,7 @@ msgid "Play"
msgstr "Відтворити"
msgid "Please %{link_to_register} or %{link_to_sign_in} to comment"
-msgstr ""
+msgstr "Будь ласка, %{link_to_register} або %{link_to_sign_in} щоб прокоментувати"
msgid "Please accept the Terms of Service before continuing."
msgstr "Будь ласка, для продовження прийміть умови надання послуг."
@@ -7095,9 +7513,21 @@ msgstr "Будь ласка сконвертуйте їх в %{link_to_git} і
msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again."
msgstr "Будь ласка сконвертуйте їх в Git на Google Code, і виконайте знову %{link_to_import_flow}."
+msgid "Please create a username with only alphanumeric characters."
+msgstr ""
+
msgid "Please enable and migrate to hashed storage to avoid security issues and ensure data integrity. %{migrate_link}"
msgstr ""
+msgid "Please enter a non-negative number"
+msgstr ""
+
+msgid "Please enter a number greater than %{number} (from the project settings)"
+msgstr ""
+
+msgid "Please enter a valid number"
+msgstr ""
+
msgid "Please fill in a descriptive name for your group."
msgstr "Введіть описове ім'я групи."
@@ -7107,9 +7537,18 @@ msgstr ""
msgid "Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access."
msgstr "Зверніть увагу, що ця програма не є частиною GitLab, і ви повинні впевнитися у її безпеці, перш ніж надавати доступ."
+msgid "Please provide a name"
+msgstr ""
+
+msgid "Please select and add a member"
+msgstr ""
+
msgid "Please select at least one filter to see results"
msgstr "Будь ласка виберіть хоча б один фільтр, щоб побачити результати"
+msgid "Please set a new password before proceeding."
+msgstr "Будь ласка, встановіть пароль перед продовженням."
+
msgid "Please solve the reCAPTCHA"
msgstr "Будь ласка, пройдіть reCAPTCHA"
@@ -7117,7 +7556,7 @@ msgid "Please try again"
msgstr "Будь ласка, спробуйте ще раз"
msgid "Please upgrade PostgreSQL to version 9.6 or greater. The status of the replication cannot be determined reliably with the current version."
-msgstr ""
+msgstr "Будь ласка, оновіть PostgreSQL до версії 9.6 або вище. Стан реплікації не може бути достовірно визначений у поточній версії."
msgid "Please use this form to report users to GitLab who create spam issues, comments or behave inappropriately."
msgstr "Будь ласка, використовуйте цю форму, щоб повідомляти GitLab про користувачів, які створюють задачі і коментарі зі спамом, або поводяться невідповідно."
@@ -7134,6 +7573,9 @@ msgstr "Налаштування"
msgid "Preferences|Navigation theme"
msgstr "Тема навігації"
+msgid "Preferences|This feature is experimental and translations are not complete yet"
+msgstr "Ця функція є експериментальною і переклади ще не завершені"
+
msgid "Press Enter or click to search"
msgstr "Для пошуку натисніть Enter або клікніть"
@@ -7162,7 +7604,7 @@ msgid "Prioritized label"
msgstr "Пріоритетні мітки"
msgid "Private"
-msgstr ""
+msgstr "Приватний"
msgid "Private - Project access must be granted explicitly to each user."
msgstr "Приватний — доступ до проекту повинен надаватися кожному користувачеві."
@@ -7186,13 +7628,13 @@ msgid "Profiles| You are going to change the username %{currentUsernameBold} to
msgstr "Ви збираєтеся змінити ім'я користувача %{currentUsernameBold} на %{newUsernameBold}. Профіль та проекти будуть перенаправлятися на простір імен %{newUsername}, але таке перенаправлення закінчиться, коли простір імен %{currentUsername} буде зареєстровано на іншого користувача або групу. Будь ласка, оновіть віддалені адреси в репозиторіях Git якомога швидше."
msgid "Profiles|@username"
-msgstr ""
+msgstr "@ім'я користувача"
msgid "Profiles|Account scheduled for removal."
msgstr "Обліковий запис запланований для видалення."
msgid "Profiles|Activate signin with one of the following services"
-msgstr ""
+msgstr "Активуйте вхід за допомогою однієї з наступних служб"
msgid "Profiles|Active"
msgstr ""
@@ -7213,28 +7655,28 @@ msgid "Profiles|Change username"
msgstr "Змінити ім'я користувача"
msgid "Profiles|Changing your username can have unintended side effects."
-msgstr ""
+msgstr "Зміна імені користувача може мати небажані побічні ефекти."
msgid "Profiles|Choose file..."
msgstr "Вибрати файл..."
msgid "Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information"
-msgstr ""
+msgstr "Виберіть для показу внесків до приватних репозиторіїв у вашому публічному профілі без інформації про проекти, репозиторії або організації"
msgid "Profiles|City, country"
-msgstr ""
+msgstr "Місто, країна"
msgid "Profiles|Clear status"
msgstr "Очистити статус"
msgid "Profiles|Click on icon to activate signin with one of the following services"
-msgstr ""
+msgstr "Клікніть на іконку, щоб активувати вхід за допомогою одного із наступних сервісів"
msgid "Profiles|Connect"
-msgstr ""
+msgstr "Приєднати"
msgid "Profiles|Connected Accounts"
-msgstr ""
+msgstr "Підключені облікові записи"
msgid "Profiles|Current path: %{path}"
msgstr "Поточний шлях: %{path}"
@@ -7255,7 +7697,7 @@ msgid "Profiles|Deleting an account has the following effects:"
msgstr "Видалення облікового запису несе наступні наслідки:"
msgid "Profiles|Disconnect"
-msgstr ""
+msgstr "Від'єднати"
msgid "Profiles|Do not show on profile"
msgstr "Не відображати у профілі"
@@ -7267,10 +7709,10 @@ msgid "Profiles|Edit Profile"
msgstr "Редагувати профіль"
msgid "Profiles|Enter your name, so people you know can recognize you"
-msgstr ""
+msgstr "Введіть ваше ім'я, щоб люди, яких ви знаєте, могли вас упізнати"
msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)"
-msgstr ""
+msgstr "Підвищити рівень безпеки вашого облікового запису за допомогою увімкнення двофакторної автентифікації (2FA)"
msgid "Profiles|Invalid password"
msgstr "Неправильний пароль"
@@ -7309,13 +7751,13 @@ msgid "Profiles|Set new profile picture"
msgstr "Встановити нове зображення для профілю"
msgid "Profiles|Social sign-in"
-msgstr ""
+msgstr "Вхід через соціальні мережі"
msgid "Profiles|Some options are unavailable for LDAP accounts"
msgstr "Деякі параметри недоступні для облікових записів LDAP"
msgid "Profiles|Tell us about yourself in fewer than 250 characters"
-msgstr ""
+msgstr "Розкажіть про себе в межах 250 символів"
msgid "Profiles|The maximum file size allowed is 200KB."
msgstr "Максимальний розмір файлу 200КБ."
@@ -7324,7 +7766,7 @@ msgid "Profiles|This doesn't look like a public SSH key, are you sure you want t
msgstr "Це не схоже на публічниц ключ SSH. Ви впевнені, що хочете його додати?"
msgid "Profiles|This email will be displayed on your public profile"
-msgstr ""
+msgstr "Ця електронна адреса буде відображатися у вашому публічному профілі"
msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}"
msgstr "Ця адреса електронної пошти буде використовуватися для браузерних операцій, таких як редагування та злиття. %{learn_more}"
@@ -7332,14 +7774,11 @@ msgstr "Ця адреса електронної пошти буде викор
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface."
msgstr "Цей смайлик та повідомлення будуть показані у вашому профілі та в інтерфейсі."
-msgid "Profiles|This feature is experimental and translations are not complete yet"
-msgstr ""
-
msgid "Profiles|This information will appear on your profile"
-msgstr ""
+msgstr "Ця інформація буде відображатися у вашому профілі"
msgid "Profiles|Two-Factor Authentication"
-msgstr ""
+msgstr "Двофакторна автентифікація"
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr "Введіть ваш %{confirmationValue} для підтвердження:"
@@ -7366,13 +7805,13 @@ msgid "Profiles|Username successfully changed"
msgstr "Ім’я користувача успішно збережено"
msgid "Profiles|Using emojis in names seems fun, but please try to set a status message instead"
-msgstr ""
+msgstr "Використання смайликів в іменах виглядає дотепно, але, будь ласка, краще використовуйте їх в повідомленнях про статус"
msgid "Profiles|What's your status?"
msgstr "Який ваш статус?"
msgid "Profiles|Who you represent or work for"
-msgstr ""
+msgstr "Кого ви представляєте або на кого працюєте"
msgid "Profiles|You can change your avatar here"
msgstr "Тут ви можете змінити свій аватар"
@@ -7393,19 +7832,19 @@ msgid "Profiles|You must transfer ownership or delete these groups before you ca
msgstr "Вам необхідно змінити власника або видалити ці групи перед тим як видалити ваш обліковий запис."
msgid "Profiles|Your LinkedIn profile name from linkedin.com/in/profilename"
-msgstr ""
+msgstr "Ваше ім'я профілю LinkedIn з linkedin.com/in/profilename"
msgid "Profiles|Your account is currently an owner in these groups:"
msgstr "Ваш обліковий запис є власником в цих групах:"
msgid "Profiles|Your email address was automatically set based on your %{provider_label} account"
-msgstr ""
+msgstr "Ваша адреса електронної пошти була автоматично встановлена на основі вашого облікового запису %{provider_label}"
msgid "Profiles|Your location was automatically set based on your %{provider_label} account"
-msgstr ""
+msgstr "Ваше місцезнаходження було автоматично встановлено на основі вашого облікового запису %{provider_label}"
msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you"
-msgstr ""
+msgstr "Ваше ім’я було автоматично встановлено на основі вашого облікового запису %{provider_label} щоб люди могли вас впізнати"
msgid "Profiles|Your status"
msgstr "Ваш статус"
@@ -7414,10 +7853,10 @@ msgid "Profiles|e.g. My MacBook key"
msgstr "наприклад, мій ключ MacBook"
msgid "Profiles|username"
-msgstr ""
+msgstr "ім'я користувача"
msgid "Profiles|website.com"
-msgstr ""
+msgstr "сайт.укр"
msgid "Profiles|your account"
msgstr "ваш обліковий запис"
@@ -7434,6 +7873,9 @@ msgstr "Прогрес"
msgid "Project"
msgstr "Проект"
+msgid "Project \"%{name}\" is no longer available. Select another project to continue."
+msgstr "Проект \"%{name}\" більше не доступний. Щоб продовжити виберіть інший проект."
+
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr "Проект '%{project_name}' перебуває в процесі видалення."
@@ -7476,8 +7918,11 @@ msgstr "Закінчився термін дії посилання на про
msgid "Project export started. A download link will be sent by email."
msgstr "Розпочато експорт проекту. Посилання для скачування буде надіслана електронною поштою."
+msgid "Project has too many %{label_for_message} to search"
+msgstr "В проекті занадто багато %{label_for_message} для пошуку"
+
msgid "Project members"
-msgstr ""
+msgstr "Учасники проекту"
msgid "Project name"
msgstr "Назва проекту"
@@ -7486,7 +7931,7 @@ msgid "Project slug"
msgstr "Шлях проекту"
msgid "Project:"
-msgstr ""
+msgstr "Проект:"
msgid "ProjectActivityRSS|Subscribe"
msgstr "Підписатися"
@@ -7575,12 +8020,18 @@ msgstr "Користувачі можуть відправляти в цей р
msgid "Projects"
msgstr "Проекти"
+msgid "Projects Successfully Retrieved"
+msgstr ""
+
msgid "Projects shared with %{group_name}"
msgstr "Спільні проекти з %{group_name}"
msgid "Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group."
msgstr "Проекти, що належать до групи, використовують її ім'я як префікс. Існуючі проекти можуть бути переміщені в групу."
+msgid "Projects with write access"
+msgstr ""
+
msgid "ProjectsDropdown|Frequently visited"
msgstr "Часто відвідувані"
@@ -7657,7 +8108,7 @@ msgid "PrometheusService|Custom metrics"
msgstr "Власні метрики"
msgid "PrometheusService|Enable Prometheus to define custom metrics, using either option above"
-msgstr ""
+msgstr "Увімкнути Prometheus для визначення власних метрик, використовуючи будь-який з варіантів вище"
msgid "PrometheusService|Finding and configuring metrics..."
msgstr "Пошук та налаштування метрик..."
@@ -7789,7 +8240,7 @@ msgid "Pseudonymizer data collection"
msgstr "Збір даних Pseudonymizer"
msgid "Public"
-msgstr ""
+msgstr "Публічний"
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr "Публічна — група та всі публічні проекти можуть переглядатися без автентифікації."
@@ -7797,6 +8248,9 @@ msgstr "Публічна — група та всі публічні проек
msgid "Public - The project can be accessed without any authentication."
msgstr "Публічний — проект может переглядатися без автентифікації."
+msgid "Public deploy keys (%{deploy_keys_count})"
+msgstr ""
+
msgid "Public pipelines"
msgstr "Публічні конвеєри"
@@ -7831,19 +8285,19 @@ msgid "Quarters"
msgstr "Квартали"
msgid "Query"
-msgstr ""
+msgstr "Запит"
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr "Швидкі дії можна використовувати в описах задач і коментарях."
msgid "README"
-msgstr ""
+msgstr "Інструкція (README)"
msgid "Read more"
msgstr "Докладніше"
msgid "Read more about environments"
-msgstr ""
+msgstr "Читати більше про середовища"
msgid "Read more about project permissions <strong>%{link_to_help}</strong>"
msgstr "Дізнайтеся більше про права доступу в проекті <strong>%{link_to_help}</strong>"
@@ -7852,6 +8306,12 @@ msgid "Real-time features"
msgstr "Фунції реального часу"
msgid "Receive alerts from manually configured Prometheus servers."
+msgstr "Отримувати попередження від налаштованого вручну сервера Prometheus."
+
+msgid "Recent"
+msgstr ""
+
+msgid "Recent Project Activity"
msgstr ""
msgid "Recent searches"
@@ -7883,7 +8343,7 @@ msgid "Register / Sign In"
msgstr "Зареєструватися / Увійти"
msgid "Register U2F device"
-msgstr ""
+msgstr "Зареєструвати пристрій U2F"
msgid "Register and see your runners for this group."
msgstr "Зареєструйте і переглядайте ваші Runner’и для цієї групи."
@@ -7916,7 +8376,7 @@ msgid "Related merge requests"
msgstr "Пов'язані запити на злиття"
msgid "Releases"
-msgstr ""
+msgstr "Релізи"
msgid "Releases mark specific points in a project's development history, communicate information about the type of change, and deliver on prepared, often compiled, versions of the software to be reused elsewhere. Currently, releases can only be created through the API."
msgstr ""
@@ -7931,10 +8391,16 @@ msgid "Remove Runner"
msgstr "Видалити Runner"
msgid "Remove all approvals in a merge request when new commits are pushed to its source branch"
-msgstr ""
+msgstr "Видаляти всі затвердження у запитах на злиття, коли у гілці-джерелі з'являються нові коміти"
msgid "Remove approver"
-msgstr ""
+msgstr "Видалити затверджуючу особу"
+
+msgid "Remove approvers"
+msgstr "Видалити затверджуючих осіб"
+
+msgid "Remove approvers?"
+msgstr "Видалити затверджуючих осіб?"
msgid "Remove avatar"
msgstr "Видалити аватар"
@@ -7967,13 +8433,13 @@ msgid "Reopen epic"
msgstr "Повторне відкриття епіку"
msgid "Reopen milestone"
-msgstr ""
+msgstr "Повторне відкриття етапу"
msgid "Repair authentication"
-msgstr "Відновити аутентифікацію"
+msgstr "Відновити автентифікацію"
msgid "Reply to comment"
-msgstr ""
+msgstr "Відповісти на коментар"
msgid "Reply to this email directly or %{view_it_on_gitlab}."
msgstr "Відповісти на це електронне повідомлення безпосередньо або %{view_it_on_gitlab}."
@@ -8023,6 +8489,12 @@ msgstr "Результати для тестового звіту обробля
msgid "Reports|Vulnerability"
msgstr "Вразливість"
+msgid "Reports|issue"
+msgstr ""
+
+msgid "Reports|merge request"
+msgstr ""
+
msgid "Reports|no changed test results"
msgstr "результати тестів не змінилися"
@@ -8036,10 +8508,10 @@ msgid "Repository URL"
msgstr "URL репозиторія"
msgid "Repository cleanup"
-msgstr ""
+msgstr "Очищення репозиторію"
msgid "Repository cleanup has started. You will receive an email once the cleanup operation is complete."
-msgstr ""
+msgstr "Очистку репозиторію розпочато. Ви отримаєте повідомлення по електронній пошті після її завершення."
msgid "Repository has no locks."
msgstr "Репозиторій не має блокувань."
@@ -8071,11 +8543,28 @@ msgstr "Вимагати від всіх користувачів цієї гр
msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
msgstr "Вимагати від усіх користувачів приймати умови надання послуг та політику конфіденційності, коли вони отримують доступ до GitLab."
-msgid "Resend invite"
+msgid "Require approval from code owners"
msgstr ""
+msgid "Requires approval from %{names}."
+msgid_plural "Requires %{count} more approvals from %{names}."
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Requires approval."
+msgid_plural "Requires %d more approvals."
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Resend invite"
+msgstr "Повторно надіслати запрошення"
+
msgid "Reset authorization key"
-msgstr ""
+msgstr "Скинути ключ авторизації"
msgid "Reset authorization key?"
msgstr ""
@@ -8102,10 +8591,10 @@ msgid "Resolve discussion"
msgstr "Завершити обговорення"
msgid "Resolved"
-msgstr ""
+msgstr "Вирішено"
msgid "Response"
-msgstr ""
+msgstr "Відповідь"
msgid "Response metrics (AWS ELB)"
msgstr "Метрики відповідей (AWS ELB)"
@@ -8117,7 +8606,7 @@ msgid "Response metrics (HA Proxy)"
msgstr "Метрики відповідей (HA Proxy)"
msgid "Response metrics (NGINX Ingress VTS)"
-msgstr ""
+msgstr "Метрики відповідей (NGINX Ingress VTS)"
msgid "Response metrics (NGINX Ingress)"
msgstr "Метрики відповідей (NGINX Ingress)"
@@ -8126,7 +8615,7 @@ msgid "Response metrics (NGINX)"
msgstr "Метрики відповідей (NGINX)"
msgid "Restart Terminal"
-msgstr ""
+msgstr "Перезапустити термінал"
msgid "Resume"
msgstr "Продовжити"
@@ -8148,7 +8637,7 @@ msgstr[2] "Показати значень"
msgstr[3] "Показати значень"
msgid "Reveal values"
-msgstr ""
+msgstr "Показати значення"
msgid "Revert this commit"
msgstr "Анулювати цей коміт"
@@ -8178,7 +8667,7 @@ msgid "Run CI/CD pipelines for external repositories"
msgstr "Запустити CI/CD конвеєри для зовнішніх репозиторіїв"
msgid "Run tests against your code live using the Web Terminal"
-msgstr ""
+msgstr "Протестувати ваш запущений код за допомогою Веб-терміналу"
msgid "Run untagged jobs"
msgstr "Виконати завдання без тегів"
@@ -8208,7 +8697,7 @@ msgid "Runners API"
msgstr "API Runner’ів"
msgid "Runners activated for this project"
-msgstr ""
+msgstr "Runner'и активовані для цього проекту"
msgid "Runners can be placed on separate users, servers, and even on your local machine."
msgstr "Runner’и можуть розміщуватися у різних користувачів, на серверах і навіть на вашій локальній машині."
@@ -8231,6 +8720,9 @@ msgstr "Ви використали усі виділенні хвилини д
msgid "Running"
msgstr "Виконується"
+msgid "Running…"
+msgstr ""
+
msgid "SAML SSO"
msgstr "Єдиний вхід SAML"
@@ -8244,7 +8736,7 @@ msgid "SAML Single Sign On Settings"
msgstr "Налаштування єдиного входу SAML"
msgid "SAML for %{group_name}"
-msgstr ""
+msgstr "SAML для %{group_name}"
msgid "SHA1 fingerprint of the SAML token signing certificate. Get this from your identity provider, where it can also be called \"Thumbprint\"."
msgstr "Відбиток SHA1 сертифікату для підписування токенів SAML. Отримайте його від провайдера ідентифікації, де він також може називатися \"Thumbprint\"."
@@ -8261,11 +8753,14 @@ msgstr "Відкритий SSH-ключ"
msgid "SSL Verification"
msgstr "Перевірка SSL"
+msgid "Saturday"
+msgstr "Субота"
+
msgid "Save"
msgstr "Зберегти"
msgid "Save Changes"
-msgstr ""
+msgstr "Зберегти зміни"
msgid "Save application"
msgstr "Зберегти застосунок"
@@ -8277,7 +8772,7 @@ msgid "Save changes before testing"
msgstr "Зберегти зміни перед тестуванням"
msgid "Save comment"
-msgstr ""
+msgstr "Зберегти коментар"
msgid "Save pipeline schedule"
msgstr "Зберегти розклад конвеєра"
@@ -8294,6 +8789,9 @@ msgstr "Заплановано"
msgid "Schedules"
msgstr "Розклади"
+msgid "Scheduling"
+msgstr ""
+
msgid "Scheduling Pipelines"
msgstr "Планування конвеєрів"
@@ -8316,7 +8814,7 @@ msgid "Search"
msgstr "Пошук"
msgid "Search an environment spec"
-msgstr ""
+msgstr "Пошук специфікації середовища"
msgid "Search branches"
msgstr "Пошук у гілках"
@@ -8331,7 +8829,7 @@ msgid "Search for projects, issues, etc."
msgstr "Пошук в проектах, задачах і т. д."
msgid "Search groups"
-msgstr ""
+msgstr "Пошук в групах"
msgid "Search merge requests"
msgstr "Пошук у запитах на злиття"
@@ -8354,6 +8852,9 @@ msgstr "Пошук проектів"
msgid "Search users"
msgstr "Пошук користувачів"
+msgid "Search users or groups"
+msgstr ""
+
msgid "Search your projects"
msgstr "Пошук у ваших проектах"
@@ -8403,7 +8904,7 @@ msgid "Security Dashboard|Issue Created"
msgstr "Створено задачу"
msgid "Security Reports|At this time, the security dashboard only supports SAST and dependency scanning."
-msgstr ""
+msgstr "На даний момент, панель безпеки підтримує тільки SAST та сканування вразливостей."
msgid "Security Reports|Create issue"
msgstr "Створити задачу"
@@ -8412,13 +8913,13 @@ msgid "Security Reports|Dismiss vulnerability"
msgstr "Відхилити вразливість"
msgid "Security Reports|Learn more about setting up your dashboard"
-msgstr ""
+msgstr "Дізнайтеся більше про налаштування вашої панелі"
msgid "Security Reports|More info"
msgstr "Детальніше"
msgid "Security Reports|No Vulnerabilities"
-msgstr ""
+msgstr "Немає вразливостей"
msgid "Security Reports|Security dashboard documentation"
msgstr "Документація до панелі безпеки"
@@ -8426,6 +8927,9 @@ msgstr "Документація до панелі безпеки"
msgid "Security Reports|There was an error creating the issue."
msgstr "Помилка при створенні задачі."
+msgid "Security Reports|There was an error creating the merge request."
+msgstr ""
+
msgid "Security Reports|There was an error dismissing the vulnerability."
msgstr "Помилка при відхиленні вразливості."
@@ -8436,16 +8940,16 @@ msgid "Security Reports|There was an error reverting this dismissal."
msgstr "Помилка при анулюванні цього відхилення."
msgid "Security Reports|Undo dismiss"
-msgstr ""
+msgstr "Відмінити відхилення"
msgid "Security Reports|We've found no vulnerabilities for your group"
-msgstr ""
+msgstr "Ми не виявили вразливостей для вашої групи"
msgid "Security Reports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
+msgstr "Хоча й рідко, але можливо, що ваша група не має вразливостей. В будь-якому разі, ми просимо вас перевірити ваші налаштування, щоб впевнитися, що ваша панель налаштована правильно."
msgid "Security dashboard"
-msgstr ""
+msgstr "Панель безпеки"
msgid "SecurityDashboard| The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr "На панелі безпеки відображається останній звіт про безпеку. Використовуйте його для пошуку та виправлення вразливостей."
@@ -8457,7 +8961,7 @@ msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered"
msgstr "Конвеєр %{pipelineLink} запущено"
msgid "See metrics"
-msgstr ""
+msgstr "Переглянути метрики"
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
@@ -8474,6 +8978,12 @@ msgstr "Виберіть групу для запрошення"
msgid "Select a namespace to fork the project"
msgstr "Виберіть простір імен для форку проекту"
+msgid "Select a project to read Insights configuration file"
+msgstr ""
+
+msgid "Select a repository"
+msgstr ""
+
msgid "Select a template repository"
msgstr "Вибрати шаблон репозиторію"
@@ -8490,7 +9000,7 @@ msgid "Select branch/tag"
msgstr "Виберіть гілку або тег"
msgid "Select members to invite"
-msgstr ""
+msgstr "Виберіть учасників для запрошення"
msgid "Select project"
msgstr "Вибрати проект"
@@ -8519,20 +9029,17 @@ msgstr "Вкажіть групу, де розміщені власні шабл
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr "При виборі користувача Gitlab посилання на нього буде додане до опису задачі та коментарів (напр. \"<a href=\"#\"> @johnsmith</a>\"). Також це призведе до асоціації та/або призначення цих задач та коментарів на вибраного користувача."
-msgid "Selective synchronization"
-msgstr "Вибіркова синхронізація"
-
msgid "Send email"
msgstr "Надіслати листа"
msgid "Send report"
-msgstr ""
+msgstr "Надіслати звіт"
msgid "Send usage data"
msgstr "Відправити дані про використання"
msgid "Sentry API URL"
-msgstr ""
+msgstr "URL-адреса Sentry API"
msgid "Sep"
msgstr "вер."
@@ -8544,46 +9051,46 @@ msgid "Server version"
msgstr "Версія сервера"
msgid "Serverless"
-msgstr ""
+msgstr "Serverless"
msgid "ServerlessDetails|Kubernetes Pods"
-msgstr ""
+msgstr "Pod'и Kubernetes"
msgid "ServerlessDetails|Number of Kubernetes pods in use over time based on necessity."
-msgstr ""
+msgstr "Кількість pod'ів Kubernetes у використанні на основі необхідності протягом періоду часу."
msgid "ServerlessDetails|pod in use"
-msgstr ""
+msgstr "pod у використанні"
msgid "ServerlessDetails|pods in use"
-msgstr ""
+msgstr "pod'и у використанні"
msgid "ServerlessURL|Copy URL to clipboard"
-msgstr ""
+msgstr "Скопіювати URL-адресу в буфер обміну"
msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
-msgstr ""
+msgstr "Для того, щоб почати використовувати функції як сервіс, необхідно спочатку встановити Knative на ваш кластер Kubernetes."
msgid "Serverless|An error occurred while retrieving serverless components"
-msgstr ""
+msgstr "Сталася помилка при отриманні компонентів serverless"
msgid "Serverless|Getting started with serverless"
-msgstr ""
+msgstr "Початок роботи із serverless"
msgid "Serverless|If you believe none of these apply, please check back later as the function data may be in the process of becoming available."
-msgstr ""
+msgstr "Якщо ви вважаєте, що жодна з них не підходить, будь ласка перевірте пізніше, тому що дані про функції можуть бути в процесі отримання."
msgid "Serverless|Install Knative"
-msgstr ""
+msgstr "Встановити Knative"
msgid "Serverless|Learn more about Serverless"
-msgstr ""
+msgstr "Дізнайтеся більше про Serverless"
msgid "Serverless|No functions available"
-msgstr ""
+msgstr "Немає доступних функцій"
msgid "Serverless|There is currently no function data available from Knative. This could be for a variety of reasons including:"
-msgstr ""
+msgstr "Наразі немає даних про функції від Knative. Це може бути викликано різними причинами, зокрема:"
msgid "Service Desk"
msgstr "Service Desk"
@@ -8597,6 +9104,9 @@ msgstr "URL сервісу"
msgid "Session expiration, projects limit and attachment size."
msgstr "Термін дії сесії, проектні ліміти та розміри вкладень."
+msgid "Set a number of approvals required, the approvers and other approval settings."
+msgstr ""
+
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "Встановіть пароль для свого облікового запису, щоб мати можливість відправляти та отримувати через %{protocol}."
@@ -8612,11 +9122,14 @@ msgstr "Встановити репозиторій шаблонів для вс
msgid "Set max session time for web terminal."
msgstr "Максимальний термін дії сесії для веб-терміналу."
+msgid "Set new password"
+msgstr ""
+
msgid "Set notification email for abuse reports."
msgstr "Налаштувати сповіщення по електронній пошті для повідомлень про зловживання."
msgid "Set number of approvers required before open merge requests can be merged"
-msgstr ""
+msgstr "Встановити необхідну кількість затверджуючих осіб перед тим, як відкритий запит на злиття може бути злитий"
msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authentication."
msgstr "Встановіть вимоги для входу користувачів. Увімкніть обов’язкову двофакторну автентифікацію."
@@ -8634,6 +9147,9 @@ msgid "Set up assertions/attributes/claims (email, first_name, last_name) and Na
msgstr "Налаштуйте твердження/атрибути (email, ім'я, прізвище) і NameID відповідно до %{docsLinkStart} документації %{icon}%{docsLinkEnd}"
msgid "Set up new U2F device"
+msgstr "Налаштувати новий пристрій U2F"
+
+msgid "Set up new password"
msgstr ""
msgid "Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically."
@@ -8693,14 +9209,20 @@ msgstr "Обнулити використані хвилини в конвеєр
msgid "Sherlock Transactions"
msgstr "Sherlock транзакції"
+msgid "Show all activity"
+msgstr ""
+
msgid "Show command"
msgstr "Показати команду"
+msgid "Show comments only"
+msgstr ""
+
msgid "Show complete raw log"
msgstr "Показати повний неформатований журнал"
msgid "Show file browser"
-msgstr ""
+msgstr "Показати файловий менеджер"
msgid "Show latest version"
msgstr "Показати останню версію"
@@ -8743,19 +9265,19 @@ msgid "Sign in / Register"
msgstr "Увійти або зареєструватися"
msgid "Sign in to \"%{group_name}\""
-msgstr ""
+msgstr "Увійти до \"%{group_name}\""
msgid "Sign in using smart card"
-msgstr ""
+msgstr "Увійти за допомогою смарт-карти"
msgid "Sign in via 2FA code"
-msgstr ""
+msgstr "Увійти за допомогою коду двофакторної автентифікції"
msgid "Sign in with Single Sign-On"
msgstr "Увійти за допомогою єдиного входу"
msgid "Sign in with smart card"
-msgstr ""
+msgstr "Увійти за допомогою смарт-карти"
msgid "Sign out"
msgstr "Вийти"
@@ -8767,7 +9289,7 @@ msgid "Sign-up restrictions"
msgstr "Обмеження для реєстрації"
msgid "Similar issues"
-msgstr ""
+msgstr "Подібні задачі"
msgid "Size"
msgstr "Розмір"
@@ -8791,17 +9313,35 @@ msgid "Smartcard authentication failed: client certificate header is missing."
msgstr "Не вдалося автентифікувати смарт-карту: відсутній заголовок в сертифікаті клієнта."
msgid "Snippet Contents"
-msgstr ""
+msgstr "Вміст сніпета"
msgid "Snippets"
msgstr "Сніпети"
-msgid "Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again."
+msgid "SnippetsEmptyState|Explore public snippets"
msgstr ""
-msgid "Someone edited this merge request at the same time you did. Please refresh the page to see changes."
+msgid "SnippetsEmptyState|New snippet"
msgstr ""
+msgid "SnippetsEmptyState|No snippets found"
+msgstr ""
+
+msgid "SnippetsEmptyState|Snippets are small pieces of code or notes that you want to keep."
+msgstr ""
+
+msgid "SnippetsEmptyState|There are no snippets to show."
+msgstr ""
+
+msgid "SnippetsEmptyState|They can be either public or private."
+msgstr ""
+
+msgid "Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again."
+msgstr "Хтось відредагував цю %{issueType} одночасно з вами. Опис було оновлено, тому вам знову треба внести свої зміни."
+
+msgid "Someone edited this merge request at the same time you did. Please refresh the page to see changes."
+msgstr "Хтось відредагував цей запит на злиття одночасно з вами. Будь ласка, оновіть сторінку, щоб побачити зміни."
+
msgid "Something went wrong on our end"
msgstr "Щось пішло не так з нашого боку"
@@ -8812,7 +9352,7 @@ msgid "Something went wrong on our end. Please try again!"
msgstr "Щось пішло не так на нашій стороні. Будь-ласка, спробуйте ще раз!"
msgid "Something went wrong on our end. Please try again."
-msgstr ""
+msgstr "Щось пішло не так на нашій стороні. Будь-ласка, спробуйте ще раз."
msgid "Something went wrong trying to change the confidentiality of this issue"
msgstr "Помилка при зміні конфіденційності цієї задачі"
@@ -8824,13 +9364,13 @@ msgid "Something went wrong when toggling the button"
msgstr "Помилка при перемиканні кнопки"
msgid "Something went wrong while applying the suggestion. Please try again."
-msgstr ""
+msgstr "Помилка при застосуванні пропозиції. Будь ласка, спробуйте знову."
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr "Помилка при закритті %{issuable}. Будь ласка, спробуйте пізніше"
msgid "Something went wrong while deleting the source branch. Please try again."
-msgstr ""
+msgstr "Помилка при видаленні гілки-джерела. Будь ласка, спробуйте знову."
msgid "Something went wrong while fetching %{listType} list"
msgstr "Помилка при отриманні списку %{listType}"
@@ -8851,7 +9391,7 @@ msgid "Something went wrong while fetching the registry list."
msgstr "Щось пішло не так при отриманні списку із реєстру."
msgid "Something went wrong while merging this merge request. Please try again."
-msgstr ""
+msgstr "Помилка при застосуванні цього запиту на злиття. Будь ласка, спробуйте знову."
msgid "Something went wrong while reopening the %{issuable}. Please try again later"
msgstr "Помилка при повторному відкритті %{issuable}. Будь ласка, спробуйте пізніше"
@@ -8878,13 +9418,13 @@ msgid "Sorry, no projects matched your search"
msgstr "На жаль жоден проект не задовольняє критеріям вашого пошуку"
msgid "Sorry, your filter produced no results"
-msgstr ""
+msgstr "На жаль, немає результатів, які відповідають фільтру"
msgid "Sort by"
msgstr "Сортувати за"
msgid "Sort direction"
-msgstr ""
+msgstr "Порядок сортування"
msgid "SortOptions|Access level, ascending"
msgstr "Рівень доступу, в порядку зростання"
@@ -8932,7 +9472,7 @@ msgid "SortOptions|Less weight"
msgstr "Менша вага"
msgid "SortOptions|Milestone due date"
-msgstr ""
+msgstr "Дата завершення етапу"
msgid "SortOptions|Milestone due later"
msgstr "Етап запланований на пізніше"
@@ -8965,7 +9505,7 @@ msgid "SortOptions|Oldest joined"
msgstr "Приєднаний найраніше"
msgid "SortOptions|Oldest last activity"
-msgstr ""
+msgstr "Найстаріша остання активність"
msgid "SortOptions|Oldest sign in"
msgstr "Залогінений найраніше"
@@ -8980,7 +9520,7 @@ msgid "SortOptions|Priority"
msgstr "Пріоритет"
msgid "SortOptions|Recent last activity"
-msgstr ""
+msgstr "Найновіша остання активність"
msgid "SortOptions|Recent sign in"
msgstr "Нещодавно зареєстровані"
@@ -9010,7 +9550,7 @@ msgid "Source is not available"
msgstr "Джерело недоступне"
msgid "Source project cannot be found."
-msgstr ""
+msgstr "Проект-джерело не знайдено."
msgid "Spam Logs"
msgstr "Спам-журнал"
@@ -9028,7 +9568,7 @@ msgid "Specify the following URL during the Runner setup:"
msgstr "Зазначте наступний URL під час встановлення Runner-а:"
msgid "Squash commit message"
-msgstr ""
+msgstr "Повідомлення для об'єднаного (squash) коміту"
msgid "Squash commits"
msgstr "Виконати об'єднання (squash) комітів"
@@ -9066,12 +9606,18 @@ msgstr "Активність в обраних проектах"
msgid "Starred projects"
msgstr "Обрані проекти"
-msgid "Stars"
+msgid "StarredProjectsEmptyState|Visit a project page and press on a star icon. Then, you can find the project on this page."
msgstr ""
-msgid "Start Web Terminal"
+msgid "StarredProjectsEmptyState|You don't have starred projects yet."
msgstr ""
+msgid "Stars"
+msgstr "У обраному"
+
+msgid "Start Web Terminal"
+msgstr "Запустити Веб-Термінал"
+
msgid "Start a %{new_merge_request} with these changes"
msgstr "Почати %{new_merge_request} з цими змінами"
@@ -9082,19 +9628,19 @@ msgid "Start and due date"
msgstr "Дата початку та завершення"
msgid "Start cleanup"
-msgstr ""
+msgstr "Почати очищення"
msgid "Start date"
msgstr "Дата початку"
msgid "Start discussion"
-msgstr ""
+msgstr "Розпочати дискусію"
msgid "Start discussion & close %{noteable_name}"
-msgstr ""
+msgstr "Розпочати обговорення та закрити %{noteable_name}"
msgid "Start discussion & reopen %{noteable_name}"
-msgstr ""
+msgstr "Розпочати обговорення і повторно відкрити %{noteable_name}"
msgid "Start the Runner!"
msgstr "Запустіть Runner!"
@@ -9106,13 +9652,13 @@ msgid "Started"
msgstr "Запущений"
msgid "Started %{startsIn}"
-msgstr ""
+msgstr "Запущено %{startsIn}"
msgid "Starting..."
-msgstr ""
+msgstr "Запуск..."
msgid "Starts %{startsIn}"
-msgstr ""
+msgstr "Буде запущено %{startsIn}"
msgid "Starts at (UTC)"
msgstr "Починається о (за Грінвічем)"
@@ -9124,10 +9670,10 @@ msgid "Status"
msgstr "Статус"
msgid "Status:"
-msgstr ""
+msgstr "Статус:"
msgid "Stop Terminal"
-msgstr ""
+msgstr "Зупинити термінал"
msgid "Stop environment"
msgstr "Зупинити середовище"
@@ -9145,7 +9691,7 @@ msgid "Stopping this environment is currently not possible as a deployment is in
msgstr "Зупинка середовища наразі неможлива, тому що відбувається розгортання"
msgid "Stopping..."
-msgstr ""
+msgstr "Зупинення..."
msgid "Storage"
msgstr "Сховище"
@@ -9163,7 +9709,7 @@ msgid "Submit as spam"
msgstr "Позначити як спам"
msgid "Submit feedback"
-msgstr ""
+msgstr "Надіслати відгук"
msgid "Submit review"
msgstr "Надіслати перевірку"
@@ -9181,19 +9727,19 @@ msgid "Subscribe at project level"
msgstr "Підписатися на рівні проекту"
msgid "Subscribe to RSS feed"
-msgstr ""
+msgstr "Підписатися на RSS-канал"
msgid "Subscribe to calendar"
-msgstr ""
+msgstr "Підписатися на календар"
msgid "Subscribed"
msgstr "Ви підписані"
msgid "SubscriptionTable|Billing"
-msgstr ""
+msgstr "Білінг"
msgid "SubscriptionTable|Free"
-msgstr ""
+msgstr "Безкоштовно"
msgid "SubscriptionTable|GitLab allows you to continue using your subscription even if you exceed the number of seats you purchased. You will be required to pay for these seats upon renewal."
msgstr ""
@@ -9208,7 +9754,7 @@ msgid "SubscriptionTable|Loading subscriptions"
msgstr ""
msgid "SubscriptionTable|Manage"
-msgstr ""
+msgstr "Управління"
msgid "SubscriptionTable|Max seats used"
msgstr ""
@@ -9226,10 +9772,10 @@ msgid "SubscriptionTable|Seats owed"
msgstr ""
msgid "SubscriptionTable|Subscription end date"
-msgstr ""
+msgstr "Дата завершення підписки"
msgid "SubscriptionTable|Subscription start date"
-msgstr ""
+msgstr "Дата початку підписки"
msgid "SubscriptionTable|This is the last time the GitLab.com team was in contact with you to settle any outstanding balances."
msgstr ""
@@ -9244,28 +9790,28 @@ msgid "SubscriptionTable|This is the number of seats you will be required to pur
msgstr ""
msgid "SubscriptionTable|Trial"
-msgstr ""
+msgstr "Пробна версія"
msgid "SubscriptionTable|Trial end date"
-msgstr ""
+msgstr "Дата закінчення пробної версії"
msgid "SubscriptionTable|Trial start date"
-msgstr ""
+msgstr "Дата початку пробної версії"
msgid "SubscriptionTable|Upgrade"
-msgstr ""
+msgstr "Підвищити"
msgid "SubscriptionTable|Usage"
-msgstr ""
+msgstr "Використання"
msgid "SubscriptionTable|Usage count is performed once a day at 12:00 PM."
msgstr ""
msgid "Suggested change"
-msgstr ""
+msgstr "Пропонована зміна"
msgid "Sunday"
-msgstr ""
+msgstr "Неділя"
msgid "Support for custom certificates is disabled. Ask your system's administrator to enable it."
msgstr ""
@@ -9276,6 +9822,9 @@ msgstr "Перейти в гілку/тег"
msgid "Sync information"
msgstr "Інформація про синхронізацію"
+msgid "System"
+msgstr ""
+
msgid "System Hooks"
msgstr "Системні гуки"
@@ -9286,7 +9835,7 @@ msgid "System default (%{default})"
msgstr ""
msgid "System header and footer"
-msgstr ""
+msgstr "Системний заголовок та футер"
msgid "System metrics (Custom)"
msgstr "Системні метрики (Власні)"
@@ -9295,10 +9844,10 @@ msgid "System metrics (Kubernetes)"
msgstr "Системні метрики (Kubernetes)"
msgid "Tag"
-msgstr ""
+msgstr "Тег"
msgid "Tag list:"
-msgstr ""
+msgstr "Список тегів:"
msgid "Tags"
msgstr "Теги"
@@ -9394,10 +9943,10 @@ msgid "Templates"
msgstr "Шаблони"
msgid "Terminal"
-msgstr ""
+msgstr "Термінал"
msgid "Terminal for environment"
-msgstr ""
+msgstr "Термінал для середовища"
msgid "Terms of Service Agreement and Privacy Policy"
msgstr "Угода про надання послуг і політика конфіденційності"
@@ -9421,7 +9970,7 @@ msgid "The Advanced Global Search in GitLab is a powerful search service that sa
msgstr "Розширений глобальний пошук в GitLab — це потужний інструмент який заощаджує ваш час. Замість дублювання коду і витрати часу, ви можете шукати код інших команд, який може допомогти у вашому проекті."
msgid "The CSV export will be created in the background. Once finished, it will be sent to <strong>%{email}</strong> in an attachment."
-msgstr ""
+msgstr "Експорт CSV буде створено у фоновому режимі. Після завершення його буде надіслано на <strong>%{email}</strong> у вкладенні."
msgid "The Git LFS objects will <strong>not</strong> be synced."
msgstr "Об'єкти Git LFS <strong>не</strong> будуть синхронізуватися."
@@ -9435,6 +9984,9 @@ msgstr "Трекер задач — це місце, де можна додат
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr "Сертифікат X509 використовується для взаємної перевірки автентичності TLS і необхідний для зв'язку з зовнішньою службою авторизації. Якщо залишити порожнім, сертифікат сервера буде перевірятись при доступі через HTTPS."
+msgid "The branch for this project has no active pipeline configuration."
+msgstr ""
+
msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git."
msgstr "Підсвітка символів дозволяє обмежувати заголовок до %{titleLength} символів і обмежувати довжину рядків тіла %{bodyLength} символами для того, щоб вони залишаються читабельними в git."
@@ -9460,11 +10012,14 @@ msgid "The issue stage shows the time it takes from creating an issue to assigni
msgstr "Стадія Задача показує, скільки часу потрібно від створення задачі до включення її до якогось етапу, або додавання задачі на дошку. Почніть створювати задачі, щоб переглядати дані для цієї стадії."
msgid "The maximum file size allowed is %{size}."
-msgstr ""
+msgstr "Максимальний розмір файлу — %{size}."
msgid "The maximum file size allowed is 200KB."
msgstr "Максимальний розмір файлу — 200 Кб."
+msgid "The name %{entryName} is already taken in this directory."
+msgstr ""
+
msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
msgstr "Пароль, який потрібен для дешифрування приватного ключа. Він є необов’язковим і зберігається у зашифрованому вигляді."
@@ -9541,19 +10096,19 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet
msgstr "Середнє значення в рядку. Приклад: між 3, 5, 9, середніми 5, між 3, 5, 7, 8, середніми (5 + 7) / 2 = 6."
msgid "There are no approvers"
-msgstr ""
+msgstr "Немає затверджуючих осіб"
msgid "There are no archived projects yet"
msgstr "Наразі немає жодного архівованого проекту"
msgid "There are no closed issues"
-msgstr ""
+msgstr "Закритих задач немає"
msgid "There are no closed merge requests"
-msgstr ""
+msgstr "Закритих запитів на злиття немає"
msgid "There are no custom project templates set up for this GitLab instance. They are enabled from GitLab's Admin Area. Contact your GitLab instance administrator to setup custom project templates."
-msgstr ""
+msgstr "Власні шаблони проектів не налаштовані для цього серверу GitLab. Вони активуються на сторінці адміністрування GitLab. Зверніться до адміністратора GitLab для налаштування власних шаблонів проектів."
msgid "There are no issues to show"
msgstr "Немає задач для відображення"
@@ -9562,10 +10117,10 @@ msgid "There are no labels yet"
msgstr "Тут ще немає міток"
msgid "There are no open issues"
-msgstr ""
+msgstr "Відкритих задач немає"
msgid "There are no open merge requests"
-msgstr ""
+msgstr "Відкритих запитів на злиття немає"
msgid "There are no packages yet"
msgstr ""
@@ -9580,7 +10135,7 @@ msgid "There are no unstaged changes"
msgstr "Немає неіндексованих змін"
msgid "There was an error adding a todo."
-msgstr "Помилка при додаванні задачі."
+msgstr "Помилка при додаванні нагадування."
msgid "There was an error deleting the todo."
msgstr "Помилка при видаленні задачі."
@@ -9588,6 +10143,9 @@ msgstr "Помилка при видаленні задачі."
msgid "There was an error loading users activity calendar."
msgstr "Помилка при завантаженні календаря активності користувачів."
+msgid "There was an error saving your changes."
+msgstr ""
+
msgid "There was an error saving your notification settings."
msgstr "Помилка при збереженні ваших налаштувань сповіщень."
@@ -9604,7 +10162,7 @@ msgid "There was an error when unsubscribing from this label."
msgstr "Помилка при відписці від цієї мітки."
msgid "These existing issues have a similar title. It might be better to comment there instead of creating another similar issue."
-msgstr ""
+msgstr "Ці існуючі проблеми мають подібні заголовки. Можливо, краще додати коментар до однієї з них замість створення нової."
msgid "They can be managed using the %{link}."
msgstr "Ними можна керувати за допомогою %{link}."
@@ -9613,10 +10171,10 @@ msgid "Third party offers"
msgstr "Сторонні пропозиції"
msgid "This %{issuable} is locked. Only <strong>project members</strong> can comment."
-msgstr ""
+msgstr "Ця %{issuable} заблокована. Лише <strong>учасники проекту</strong> можуть коментувати."
msgid "This %{viewer} could not be displayed because %{reason}. You can %{options} instead."
-msgstr ""
+msgstr "Цей %{viewer} не може бути відображено через %{reason}. Замість цього можна %{options}."
msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
msgstr "Цей інстанс GitLab ще немає загальних Runner'ів. Адміністратори можуть їх зареєструвати у спеціальному розділі конфігурації."
@@ -9633,6 +10191,21 @@ msgstr "Видимість цієї дошки обмежена"
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr "Ця гілка була змінена після того моменту, коли ви почали її редагувати. Ви хотіли б створити нову?"
+msgid "This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request."
+msgstr ""
+
+msgid "This commit was signed with a <strong>verified</strong> signature and the committer email is verified to belong to the same user."
+msgstr ""
+
+msgid "This commit was signed with a different user's verified signature."
+msgstr ""
+
+msgid "This commit was signed with a verified signature, but the committer email is <strong>not verified</strong> to belong to the same user."
+msgstr ""
+
+msgid "This commit was signed with an <strong>unverified</strong> signature."
+msgstr ""
+
msgid "This container registry has been scheduled for deletion."
msgstr "Заплановане видалення цього реєстру контейнерів."
@@ -9652,6 +10225,9 @@ msgid "This directory"
msgstr "Цей каталог"
msgid "This domain is not verified. You will need to verify ownership before access is enabled."
+msgstr "Цей домен не підтверджено. Щоб увімкнути доступ, потрібно підтвердити право власності."
+
+msgid "This field is required."
msgstr ""
msgid "This group"
@@ -9718,10 +10294,10 @@ msgid "This job is in pending state and is waiting to be picked by a runner"
msgstr "Це завдання перебуває в стані очікування і чекає на запуск Runner"
msgid "This job is stuck because you don't have any active runners online with any of these tags assigned to them:"
-msgstr ""
+msgstr "Це завдання заблоковане, тому що немає жодних runner'ів з будь яким із цих тегів:"
msgid "This job is stuck because you don't have any active runners that can run this job."
-msgstr ""
+msgstr "Це завдання заблоковане, тому що немає активних runner'ів, які могли б його виконати."
msgid "This job is the most recent deployment to %{link}."
msgstr "Це завдання є останнім розгортанням на %{link}."
@@ -9730,7 +10306,7 @@ msgid "This job requires a manual action"
msgstr "Завдання вимагає ручних дій"
msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
-msgstr ""
+msgstr "Завдання буде запущено автоматично після завершення таймера. Зазвичай вони використовуються для інкрементального розгортання в production середовище. У разі скасування завдання воно перетвориться на ручну операцію."
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "Це означає, що ви не можете відправляти код, поки не створите порожній репозиторій або не імпортуєте існуючий."
@@ -9738,6 +10314,12 @@ msgstr "Це означає, що ви не можете відправляти
msgid "This merge request is locked."
msgstr "Цей запит на злиття заблоковано."
+msgid "This merge request must be approved by members of these groups. You can override the project settings by setting your own list of approvers."
+msgstr "Цей запит на злиття має бути затверджений учасниками цих груп. Ви можете перевизначити проектні налаштування, встановивши ваш власний список затверджуючих осіб."
+
+msgid "This merge request must be approved by these users. You can override the project settings by setting your own list of approvers."
+msgstr "Цей запит на злиття має бути затверджений цими користувачами. Ви можете перевизначити проектні налаштування, встановивши ваш власний список затверджуючих осіб."
+
msgid "This option is disabled as you don't have write permissions for the current branch"
msgstr "Цей параметр вимкнено, тому що у вас немає дозволу на запис у поточну гілку"
@@ -9751,13 +10333,13 @@ msgid "This page will be removed in a future release."
msgstr "Цю сторінку буде видалено у майбутній версії."
msgid "This pipeline is run in a merge request context"
-msgstr ""
+msgstr "Цей конвеєр виконується в контексті запиту злиття"
msgid "This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}"
-msgstr ""
+msgstr "Цей конвеєр використовує попередньо визначену конфігурацію CI / CD, увімкнену за допомогою %{strongStart}Auto DevOps.%{strongEnd}"
msgid "This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>"
-msgstr ""
+msgstr "Цей конвеєр використовує попередньо визначену конфігурацію CI / CD, увімкнену за допомогою <b>Auto DevOps</b>"
msgid "This project"
msgstr "Цей проект"
@@ -9799,7 +10381,7 @@ msgid "This user will be the author of all events in the activity feed that are
msgstr "Цей користувач буде автором всіх подій в каналі активності, які є результатом оновлення, наприклад створення нових гілок або відправлення нових комітів до існуючих гілок. При створенні або перепризначенні ви зможете призначити лише себе користувачем для віддзеркалення."
msgid "This will redirect you to an external sign in page."
-msgstr ""
+msgstr "Це перенаправить вас на зовнішню сторінку входу."
msgid "Those emails automatically become issues (with the comments becoming the email conversation) listed here."
msgstr "Ці повідомлення електронної пошти автоматично стануть обговореннями задач, які відображатимуться тут (причому коментарі стануть частиною переписки)."
@@ -9996,7 +10578,7 @@ msgid "Title"
msgstr "Заголовок"
msgid "Titles and Filenames"
-msgstr ""
+msgstr "Заголовки та імена файлів"
msgid "To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration."
msgstr ""
@@ -10005,7 +10587,7 @@ msgid "To GitLab"
msgstr "В GitLab"
msgid "To access this domain create a new DNS record"
-msgstr ""
+msgstr "Щоб отримати доступ до цього домену, створіть новий запис DNS"
msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
msgstr "Для того, щоб додати ключ SSH вам необхідно %{generate_link_start}згенерувати його%{link_end} або використати %{existing_link_start}існуючий ключ%{link_end}."
@@ -10056,7 +10638,7 @@ msgid "To keep this project going, create a new merge request"
msgstr ""
msgid "To link Sentry to GitLab, enter your Sentry URL and Auth Token."
-msgstr ""
+msgstr "Для інтеграції Sentry із Gitlab введіть URL-адресу Sentry та токен автентифікації."
msgid "To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here."
msgstr "Щоб перемістити або скопіювати весь проект GitLab з іншої інсталяції GitLab до цього, перейдіть на сторінку налаштувань оригіналу проекту, створіть файл експорту та надішліть його сюди."
@@ -10068,19 +10650,19 @@ msgid "To open Jaeger and easily view tracing from GitLab, link the %{link} page
msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
-msgstr ""
+msgstr "Для збереження швидкодії відображаються лише <strong>%{display_size} із %{real_size}</strong> файлів."
msgid "To receive alerts from manually configured Prometheus services, add the following URL and Authorization key to your Prometheus webhook config file. Learn more about %{linkStart}configuring Prometheus%{linkEnd} to send alerts to GitLab."
msgstr ""
msgid "To set up SAML authentication for your group through an identity provider like Azure, Okta, Onelogin, Ping Identity, or your custom SAML 2.0 provider:"
-msgstr "Щоб налаштувати аутентифікацію SAML для вашої групи через провайдера ідентифікації такої як Azure, Okta, Onelogin, Ping Identity або вашого власного постачальника SAML 2.0:"
+msgstr "Щоб налаштувати автентифікацію SAML для вашої групи через провайдера ідентифікації такої як Azure, Okta, Onelogin, Ping Identity або вашого власного постачальника SAML 2.0:"
msgid "To start serving your jobs you can add Runners to your group"
msgstr "Для виконання ваших завдань ви можете додати Runner’и до вашої групи"
msgid "To start serving your jobs you can either add specific Runners to your project or use shared Runners"
-msgstr ""
+msgstr "Щоб почати ваші завдання виконувалися, ви можете або додати спеціальні Runner’и до вашого проекту, або використовувати загальні"
msgid "To this GitLab instance"
msgstr "До цього інстансу GitLab"
@@ -10092,7 +10674,7 @@ msgid "To view the roadmap, add a start or due date to one of your epics in this
msgstr "Для перегляду плану-графіку, додайте дату початку чи закінчення до одного з ваших епіків в цій групі або її підгрупах. При помісячному перегляді показуються тільки епіки за попередній, поточний, та наступні 5 місяців."
msgid "To widen your search, change or remove filters above"
-msgstr ""
+msgstr "Щоб розширити пошук, змініть або видаліть фільтри вище"
msgid "To widen your search, change or remove filters."
msgstr "Щоб розширити пошук, змініть або видаліть фільтри."
@@ -10101,22 +10683,22 @@ msgid "Today"
msgstr "Сьогодні"
msgid "Todo"
-msgstr "Задача"
+msgstr "Нагдування"
msgid "Todos"
-msgstr "Задачі"
+msgstr "Нагадування"
msgid "Toggle Sidebar"
msgstr "Перемикач бічної панелі"
msgid "Toggle comments for this file"
-msgstr ""
+msgstr "Увімкнути або вимкнути коментарі для цього файлу"
msgid "Toggle commit description"
msgstr "Перемкнути опис комітів"
msgid "Toggle commit list"
-msgstr ""
+msgstr "Відкрити або закрити список комітів"
msgid "Toggle discussion"
msgstr "Перемикач дискусії"
@@ -10182,9 +10764,12 @@ msgid "Trigger this manual action"
msgstr "Запустити цю ручну дію"
msgid "Trigger token:"
-msgstr ""
+msgstr "Токен тригера:"
msgid "Trigger variables:"
+msgstr "Змінні тригера:"
+
+msgid "Triggerer"
msgstr ""
msgid "Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will impersonate their associated user including their access to projects and their project permissions."
@@ -10197,13 +10782,13 @@ msgid "Try again"
msgstr "Спробуйте ще раз"
msgid "Try again?"
-msgstr ""
+msgstr "Спробуйте ще раз?"
msgid "Try all GitLab has to offer for 30 days."
msgstr "Спробуйте всі функції GitLab протягом 30 днів."
msgid "Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now."
-msgstr ""
+msgstr "Відбувається з'єднання із вашим пристроєм. Підключіть його (якщо ви цього ще не зробили) і натисніть кнопку на ньому зараз."
msgid "Turn on Service Desk"
msgstr "Ввімкнути Service Desk"
@@ -10218,7 +10803,7 @@ msgid "Type"
msgstr "Тип"
msgid "URL"
-msgstr ""
+msgstr "URL"
msgid "Unable to load the diff. %{button_try_again}"
msgstr "Неможливо завантажити порівняння (diff). %{button_try_again}"
@@ -10230,13 +10815,13 @@ msgid "Unable to update this epic at this time."
msgstr "Неможливо оновити цей епік в даний момент."
msgid "Unblock"
-msgstr ""
+msgstr "Розблокувати"
msgid "Undo"
msgstr "Скасувати"
msgid "Unfortunately, your email message to GitLab could not be processed."
-msgstr ""
+msgstr "На жаль, ваше повідомлення електронної пошти до GitLab не може бути оброблено."
msgid "Unknown"
msgstr "Невідомо"
@@ -10287,7 +10872,7 @@ msgid "Unsubscribe at project level"
msgstr "Відписатися на рівні проекту"
msgid "Unsubscribe from %{type}"
-msgstr ""
+msgstr "Відписатися від %{type}"
msgid "Unverified"
msgstr "Непідтверджено"
@@ -10301,8 +10886,11 @@ msgstr "Незабаром"
msgid "Update"
msgstr "Оновити"
+msgid "Update approvers"
+msgstr "Оновити затверджуючих осіб"
+
msgid "Update failed"
-msgstr ""
+msgstr "Оновлення не вдалося"
msgid "Update now"
msgstr "Оновити зараз"
@@ -10310,11 +10898,17 @@ msgstr "Оновити зараз"
msgid "Update your group name, description, avatar, and visibility."
msgstr "Оновіть ім’я групи, опис, аватар та видимість."
+msgid "Update your project name, tags, description and avatar."
+msgstr ""
+
+msgid "Updated"
+msgstr ""
+
msgid "Updating"
msgstr "Оновлення"
msgid "Upgrade plan to unlock Canary Deployments feature"
-msgstr ""
+msgstr "Перейдіть на вищий тарифний план, щоб отримати доступ до функціональності Canary Deployments"
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr "Перейдіть на вищий тарифний план щоб активувати Покращений Глобальний Пошук."
@@ -10335,16 +10929,16 @@ msgid "Upload <code>GoogleCodeProjectHosting.json</code> here:"
msgstr "Надіслати <code>GoogleCodeProjectHosting.json</code> тут:"
msgid "Upload CSV file"
-msgstr ""
+msgstr "Завантажити CSV файл"
msgid "Upload New File"
msgstr "Надіслати новий файл"
msgid "Upload a certificate for your domain with all intermediates"
-msgstr ""
+msgstr "Завантажити сертифікат для вашого домену з усіма проміжними"
msgid "Upload a private key for your certificate"
-msgstr ""
+msgstr "Завантажити приватний ключ для сертифіката"
msgid "Upload file"
msgstr "Надіслати файл"
@@ -10389,13 +10983,13 @@ msgid "Use your global notification setting"
msgstr "Використовуються глобальні налаштування повідомлень"
msgid "Use your smart card to authenticate with the LDAP server."
-msgstr ""
+msgstr "Використовуйте смарт-карту для автентифікації на сервері LDAP."
msgid "Used by members to sign in to your group in GitLab"
msgstr "Використовується учасниками для входу у вашу групу в GitLab"
msgid "Used to help configure your identity provider"
-msgstr ""
+msgstr "Використовується для допомоги в налаштуванні провайдера ідентифікації"
msgid "User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled."
msgstr "Когорти Користувачів показуються лише тоді, коли увімкнено %{usage_ping_link_start}збір даних про використання%{usage_ping_link_end}."
@@ -10422,22 +11016,22 @@ msgid "UserProfile|Edit profile"
msgstr "Редагувати профіль"
msgid "UserProfile|Explore public groups to find projects to contribute to."
-msgstr ""
+msgstr "Переглядайте публічні групи та знаходьте проекти для своїх внесків."
msgid "UserProfile|Groups"
msgstr "Групи"
msgid "UserProfile|Groups are the best way to manage projects and members."
-msgstr ""
+msgstr "Групи — це найкращий спосіб керувати проектами та учасниками."
msgid "UserProfile|Join or create a group to start contributing by commenting on issues or submitting merge requests!"
-msgstr ""
+msgstr "Приєднуйтесь до або створіть групу, щоб почати робити внески через коментарі до задач, а також надсилаючи запити на злиття!"
msgid "UserProfile|Most Recent Activity"
msgstr "Остання активність"
msgid "UserProfile|No snippets found."
-msgstr ""
+msgstr "Сніпетів не знайдено."
msgid "UserProfile|Overview"
msgstr "Огляд"
@@ -10452,19 +11046,19 @@ msgid "UserProfile|Snippets"
msgstr "Сніпети"
msgid "UserProfile|Snippets in GitLab can either be private, internal, or public."
-msgstr ""
+msgstr "Сніпети в GitLab можуть бути приватними, внутрішніми та публічними."
msgid "UserProfile|Subscribe"
msgstr "Підписатися"
msgid "UserProfile|This user doesn't have any personal projects"
-msgstr ""
+msgstr "Цей користувач не має особистих проектів"
msgid "UserProfile|This user has a private profile"
msgstr "Цей користувач має приватний профіль"
msgid "UserProfile|This user hasn't contributed to any projects"
-msgstr ""
+msgstr "Цей користувач не робив внесків до жодного із проектів"
msgid "UserProfile|View all"
msgstr "Переглянути все"
@@ -10473,31 +11067,31 @@ msgid "UserProfile|View user in admin area"
msgstr "Переглянути користувача в адмінці"
msgid "UserProfile|You can create a group for several dependent projects."
-msgstr ""
+msgstr "Ви можете створити групу для декількох залежних проектів."
msgid "UserProfile|You haven't created any personal projects."
-msgstr ""
+msgstr "Ви ще не створили жодного особистого проекту."
msgid "UserProfile|You haven't created any snippets."
-msgstr ""
+msgstr "Ви ще не створили жодних сніпетів."
msgid "UserProfile|Your projects can be available publicly, internally, or privately, at your choice."
-msgstr ""
+msgstr "За вашим вибором ваші проекти можуть бути доступні публічно, внутрішньо або приватно."
msgid "Users"
msgstr "Користувачі"
msgid "Users requesting access to"
-msgstr ""
+msgstr "Користувачі, які запитують доступ до"
msgid "Validate"
-msgstr ""
+msgstr "Перевірити"
msgid "Validate your GitLab CI configuration file"
-msgstr ""
+msgstr "Перевірити ваш файл конфігурації Gitlab CI"
msgid "Value"
-msgstr ""
+msgstr "Значення"
msgid "Various container registry settings."
msgstr "Різноманітні налаштування реєстру контейнерів."
@@ -10506,7 +11100,7 @@ msgid "Various email settings."
msgstr "Різноманітні налаштування пошти."
msgid "Various localization settings."
-msgstr ""
+msgstr "Різні параметри локалізації."
msgid "Various settings that affect GitLab performance."
msgstr "Різноманітні налаштування, що впливають на продуктивність GitLab."
@@ -10515,7 +11109,7 @@ msgid "Verification information"
msgstr "Інформація про перевірку"
msgid "Verification status"
-msgstr ""
+msgstr "Стан перевірки"
msgid "Verified"
msgstr "Підтверджено"
@@ -10530,14 +11124,17 @@ msgid "View app"
msgstr "Переглянути застосунок"
msgid "View deployment"
-msgstr ""
+msgstr "Переглянути розгортання"
msgid "View details: %{details_url}"
-msgstr ""
+msgstr "Переглянути деталі: %{details_url}"
msgid "View documentation"
msgstr "Переглянути документацію"
+msgid "View eligible approvers"
+msgstr "Переглянути доступних осіб для затвердження"
+
msgid "View epics list"
msgstr "Переглянути список епіків"
@@ -10547,8 +11144,8 @@ msgstr "Перегляд файла @ "
msgid "View group labels"
msgstr "Переглянути мітки групи"
-msgid "View issue"
-msgstr "Переглянути задачу"
+msgid "View in Sentry"
+msgstr ""
msgid "View it on GitLab"
msgstr "Переглянути це на GitLab"
@@ -10575,7 +11172,7 @@ msgid "View the documentation"
msgstr "Переглянути документацію"
msgid "Viewing commit"
-msgstr ""
+msgstr "Перегляд коміту"
msgid "Visibility and access controls"
msgstr "Налаштування видимості та доступу"
@@ -10586,6 +11183,9 @@ msgstr "Рівень видимості"
msgid "Visibility level:"
msgstr "Рівень видимості:"
+msgid "Visibility, project features, permissions"
+msgstr ""
+
msgid "Visibility:"
msgstr "Видимість:"
@@ -10602,10 +11202,10 @@ msgid "VisibilityLevel|Unknown"
msgstr "Невідомий"
msgid "Vulnerability Chart"
-msgstr ""
+msgstr "Статистика вразливостей"
msgid "Vulnerability List"
-msgstr ""
+msgstr "Список вразливостей"
msgid "Vulnerability|Class"
msgstr "Клас"
@@ -10632,7 +11232,7 @@ msgid "Vulnerability|Project"
msgstr "Проект"
msgid "Vulnerability|Report Type"
-msgstr ""
+msgstr "Тип звіту"
msgid "Vulnerability|Severity"
msgstr "Рівень"
@@ -10641,19 +11241,19 @@ msgid "Want to see the data? Please ask an administrator for access."
msgstr "Хочете побачити дані? Будь ласка, попросить у адміністратора доступ."
msgid "We can't find an epic that matches what you are looking for."
-msgstr ""
+msgstr "Ми не можемо знайти епік, який відповідає тому, що ви шукаєте."
msgid "We can't find an issue that matches what you are looking for."
-msgstr ""
+msgstr "Ми не можемо знайти задачу, яка відповідає тому, що ви шукаєте."
msgid "We could not determine the path to remove the epic"
-msgstr ""
+msgstr "Ми не змогли визначити шлях до видалення епіку"
msgid "We could not determine the path to remove the issue"
-msgstr ""
+msgstr "Ми не змогли визначити шлях до видалення задачі"
msgid "We couldn't find any results matching"
-msgstr ""
+msgstr "Не вдалося знайти відповідні результати"
msgid "We detected potential spam in the %{humanized_resource_name}. Please solve the reCAPTCHA to proceed."
msgstr "Ми виявили потенційний спам у %{humanized_resource_name}. Будь ласка, введіть цей код підтвердження reCAPTCHA, щоб продовжити."
@@ -10662,7 +11262,7 @@ msgid "We don't have enough data to show this stage."
msgstr "Ми не маємо достатньо даних для відображення цієї стадії."
msgid "We heard back from your U2F device. You have been authenticated."
-msgstr ""
+msgstr "Ми отримали відповідь від вашого пристрою U2F. Ви пройшли автентифікацію."
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "Ми хочемо бути впевнені, що це ви, будь ласка, підтвердіть, що ви не робот."
@@ -10671,7 +11271,7 @@ msgid "Web IDE"
msgstr "Веб-IDE"
msgid "Web Terminal"
-msgstr ""
+msgstr "Веб-термінал"
msgid "Web terminal"
msgstr "Веб-термінал"
@@ -10698,7 +11298,7 @@ msgid "When leaving the URL blank, classification labels can still be specified
msgstr "Якщо залишити URL порожнім, можна встановлювати мітки класифікації без вимкнення функцій проекту та виконання зовнішньої авторизації."
msgid "When:"
-msgstr ""
+msgstr "Коли:"
msgid "Who can see this group?"
msgstr "Хто може бачити цю групу?"
@@ -10842,7 +11442,7 @@ msgid "Wiki|Wiki Pages"
msgstr "Вікі-сторінки"
msgid "Will deploy to"
-msgstr ""
+msgstr "Розгорне на"
msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
msgstr "З аналітикою контриб’юторів ви може вивчати активність в задачах, запитах на злиття і подій відправки коду для вашої організації і її учасників."
@@ -10850,11 +11450,17 @@ msgstr "З аналітикою контриб’юторів ви може ви
msgid "Withdraw Access Request"
msgstr "Скасувати запит доступу"
+msgid "Write"
+msgstr ""
+
msgid "Write a comment or drag your files here…"
+msgstr "Напишіть коментар або перетягніть файли сюди…"
+
+msgid "Write access allowed"
msgstr ""
msgid "Write milestone description..."
-msgstr ""
+msgstr "Створити опис етапу..."
msgid "Yes"
msgstr "Так"
@@ -10869,7 +11475,7 @@ msgid "Yesterday"
msgstr "Вчора"
msgid "You"
-msgstr ""
+msgstr "Ви"
msgid "You are an admin, which means granting access to <strong>%{client_name}</strong> will allow them to interact with GitLab as an admin as well. Proceed with caution."
msgstr "Ви — адміністратор, а це означає, що надання доступу для <strong>%{client_name}</strong> дозволить їм взаємодіяти з GitLab як адміністратору. Продовжуйте обережно."
@@ -10890,7 +11496,7 @@ msgid "You are on a read-only GitLab instance."
msgstr "Ви знаходитеся на інстансі Gitlab \"тільки для читання\"."
msgid "You are receiving this message because you are a GitLab administrator for %{url}."
-msgstr ""
+msgstr "Ви отримуєте це повідомлення, бо ви є адміністратором GitLab для %{url}."
msgid "You can %{linkStart}view the blob%{linkEnd} instead."
msgstr "Замість цього ви можете %{linkStart}переглянути бінарні дані%{linkEnd}."
@@ -10917,7 +11523,7 @@ msgid "You can only edit files when you are on a branch"
msgstr "Ви можете редагувати файли, лише перебуваючи у якійсь гілці"
msgid "You can only merge once the items above are resolved"
-msgstr ""
+msgstr "Ви можете зливати лише коли вищезгадані пункти будуть вирішені"
msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr "Ви можете розв’язати цей конфлікт злиття за допомогою інтерактивного режиму (використовуючи кнопки %{use_ours} та %{use_theirs}), або безпосередньо редагуючи файли. Закомітити зміни у %{branch_name}"
@@ -10938,7 +11544,7 @@ msgid "You do not have any subscriptions yet"
msgstr "У вас ще немає підписок"
msgid "You do not have permission to run the Web Terminal. Please contact a project administrator."
-msgstr ""
+msgstr "Ви не маєте дозволу запускати Веб-термінал. Будь ласка, зверніться до адміністратора проекту."
msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
msgstr "У вас немає необхідних прав доступу, щоб перевизначити налаштування синхронізації LDAP-груп."
@@ -10950,16 +11556,19 @@ msgid "You don't have any authorized applications"
msgstr "Ви не маєте ніяких авторизованих застосунків"
msgid "You don't have any deployments right now."
-msgstr ""
+msgstr "Наразі у вас немає ніяких розгортань."
msgid "You have no permissions"
msgstr "У вас немає прав доступу"
+msgid "You have not added any approvers. Start by adding users or groups."
+msgstr "Ви не додали жодної затверджуючої особи. Почніть з додавання користувачів або груп."
+
msgid "You have reached your project limit"
msgstr "Ви досягли свого ліміту по кількості проектів"
msgid "You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>."
-msgstr ""
+msgstr "Ви також можете додати змінні, що будуть доступними для запущеного застосунку шляхом додавання префіксу <code>K8S_SECRET_</code> до їх імен."
msgid "You must accept our Terms of Service and privacy policy in order to register an account"
msgstr "Ви повинні прийняти правила користування сервісом і політику конфіденційності для того, щоб створити обліковий запис"
@@ -10971,7 +11580,7 @@ msgid "You need a different license to enable FileLocks feature"
msgstr "Для активації функції Блокування Файлів вам потрібна інша ліцензія"
msgid "You need a different license to enable Geo replication."
-msgstr ""
+msgstr "Вам потрібна інша ліцензія на використання Geo реплікації."
msgid "You need git-lfs version %{min_git_lfs_version} (or greater) to continue. Please visit https://git-lfs.github.com"
msgstr "Вам потрібна версія git-lfs версії %{min_git_lfs_version} (або новіша), щоб продовжити. Будь ласка, відвідайте сторінку https://git-lfs.github.com"
@@ -10980,13 +11589,13 @@ msgid "You need permission."
msgstr "Вам потрібен дозвіл"
msgid "You need to register a two-factor authentication app before you can set up a U2F device."
-msgstr ""
+msgstr "Вам треба зареєструвати програму для двофакторної автентифікації перед налаштуванням пристрою U2F."
msgid "You will lose all changes you've made to this file. This action cannot be undone."
-msgstr ""
+msgstr "Ви втратите всі зміни, внесені вами в цей файл. Цю дію не можна скасувати."
msgid "You will lose all the unstaged changes you've made in this project. This action cannot be undone."
-msgstr ""
+msgstr "Ви втратите всі неіндексовані зміни, внесені вами в цей проект. Цю дію не можна скасувати."
msgid "You will not get any notifications via email"
msgstr "Ви не отримаєте ніяких повідомлень по електронній пошті"
@@ -11015,6 +11624,9 @@ msgstr "Ви не зможете відправляти та отримуват
msgid "You'll need to use different branch names to get a valid comparison."
msgstr "Вам необхідно використовувати різні імена гілок для коректного порівняння."
+msgid "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options."
+msgstr ""
+
msgid "You're receiving this email because %{reason}."
msgstr "Ви отримали цей електронний лист, оскільки %{reason}."
@@ -11043,10 +11655,10 @@ msgid "Your Projects' Activity"
msgstr "Активність ваших проектів"
msgid "Your Todos"
-msgstr "Ваші Задачі"
+msgstr "Ваші Нагадування"
msgid "Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left."
-msgstr ""
+msgstr "Ваш пристрій U2F має бути налаштований. Підключіть його (якщо ви цього ще не зробили) і натисніть кнопку зліва."
msgid "Your applications (%{size})"
msgstr "Ваші застосунки (%{size})"
@@ -11055,7 +11667,7 @@ msgid "Your authorized applications"
msgstr "Ваші авторизовані застосунки"
msgid "Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer)."
-msgstr ""
+msgstr "Ваш браузер не підтримує U2F. Будь ласка, використовйте Google Chrome для комп'ютера (версію 41 або новішу)."
msgid "Your changes can be committed to %{branch_name} because a merge request is open."
msgstr "Ваші зміни можуть бути закомічені до %{branch_name}, оскільки запит на злиття відкритий."
@@ -11070,22 +11682,22 @@ msgid "Your comment will not be visible to the public."
msgstr "Ваш коментар не буде видимим для всіх."
msgid "Your device was successfully set up! Give it a name and register it with the GitLab server."
-msgstr ""
+msgstr "Ваш пристрій успішно налаштовано! Дайте йому ім'я та зареєструйте його на сервері GitLab."
msgid "Your groups"
msgstr "Ваші групи"
msgid "Your issues are being imported. Once finished, you'll get a confirmation email."
-msgstr ""
+msgstr "Ваші задачі імпортуються. Після завершення ви отримаєте повідомлення по електронній пошті із підтвердженням."
msgid "Your issues will be imported in the background. Once finished, you'll get a confirmation email."
-msgstr ""
+msgstr "Ваші задачі будуть імпортовані в фоні. Після завершення ви отримаєте повідомлення по електронній пошті із підтвердженням."
msgid "Your name"
msgstr "Ваше ім'я"
msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it"
-msgstr ""
+msgstr "Ваш ліміт проектів складає %{limit}! Будь ласка, зверніться до адміністратора, щоб його збільшити"
msgid "Your projects"
msgstr "Ваші проекти"
@@ -11093,6 +11705,9 @@ msgstr "Ваші проекти"
msgid "a deleted user"
msgstr "видалений користувач"
+msgid "added %{created_at_timeago}"
+msgstr ""
+
msgid "ago"
msgstr "тому"
@@ -11105,7 +11720,13 @@ msgstr "тощо"
msgid "assign yourself"
msgstr "призначити себе"
+msgid "at"
+msgstr ""
+
msgid "attach a new file"
+msgstr "прикріпити новий файл"
+
+msgid "authored"
msgstr ""
msgid "branch name"
@@ -11132,6 +11753,20 @@ msgstr "%{vulnerability} впливає на %{namespace}."
msgid "ciReport|%{remainingPackagesCount} more"
msgstr "%{remainingPackagesCount} більше"
+msgid "ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerability"
+msgid_plural "ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerabilities"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerability for the source branch only"
+msgid_plural "ciReport|%{reportType} %{status} detected %{dismissedCount} dismissed vulnerabilities for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "ciReport|%{reportType} %{status} detected %{fixedCount} fixed vulnerability"
msgid_plural "ciReport|%{reportType} %{status} detected %{fixedCount} fixed vulnerabilities"
msgstr[0] "%{reportType} %{status} виявив %{fixedCount} виправлену вразливість"
@@ -11139,6 +11774,9 @@ msgstr[1] "%{reportType} %{status} виявив %{fixedCount} виправлен
msgstr[2] "%{reportType} %{status} виявив %{fixedCount} виправлених вразливостей"
msgstr[3] "%{reportType} %{status} виявив %{fixedCount} виправлених вразливостей"
+msgid "ciReport|%{reportType} %{status} detected %{fixedCount} fixed, and %{dismissedCount} dismissed vulnerabilities"
+msgstr ""
+
msgid "ciReport|%{reportType} %{status} detected %{newCount} new vulnerability"
msgid_plural "ciReport|%{reportType} %{status} detected %{newCount} new vulnerabilities"
msgstr[0] "%{reportType} %{status} виявив %{newCount} нову вразливість"
@@ -11146,6 +11784,15 @@ msgstr[1] "%{reportType} %{status} виявив %{newCount} нові вразл
msgstr[2] "%{reportType} %{status} виявив %{newCount} нових вразливостей"
msgstr[3] "%{reportType} %{status} виявив %{newCount} нових вразливостей"
+msgid "ciReport|%{reportType} %{status} detected %{newCount} new, %{fixedCount} fixed, and %{dismissedCount} dismissed vulnerabilities"
+msgstr ""
+
+msgid "ciReport|%{reportType} %{status} detected %{newCount} new, and %{dismissedCount} dismissed vulnerabilities"
+msgstr ""
+
+msgid "ciReport|%{reportType} %{status} detected %{newCount} new, and %{dismissedCount} dismissed vulnerabilities for the source branch only"
+msgstr ""
+
msgid "ciReport|%{reportType} %{status} detected %{newCount} new, and %{fixedCount} fixed vulnerabilities"
msgstr "%{reportType} %{status} виявив %{newCount} нових та %{fixedCount} виправлених вразливостей"
@@ -11199,17 +11846,29 @@ msgstr "Якість коду"
msgid "ciReport|Confidence"
msgstr "Впевненість"
+msgid "ciReport|Container Scanning"
+msgstr ""
+
msgid "ciReport|Container scanning"
msgstr "Сканування контейнера"
msgid "ciReport|Container scanning detects known vulnerabilities in your docker images."
msgstr "Сканування контейнерів виявляє відомі вразливості у ваших Docker образах."
+msgid "ciReport|Create issue"
+msgstr ""
+
+msgid "ciReport|Create merge request"
+msgstr ""
+
+msgid "ciReport|Created %{eventType}"
+msgstr ""
+
msgid "ciReport|DAST"
msgstr "DAST"
msgid "ciReport|Dependency Scanning"
-msgstr ""
+msgstr "Сканування залежностей"
msgid "ciReport|Dependency Scanning detects known vulnerabilities in your source code's dependencies."
msgstr "Сканування залежностей виявляє відомі вразливості у залежностях вашого коду."
@@ -11227,10 +11886,10 @@ msgid "ciReport|Dismissed by"
msgstr "Відхилено"
msgid "ciReport|Download and apply the patch to fix this vulnerability."
-msgstr ""
+msgstr "Завантажити або застосувати патч, щоб виправити цю вразливість."
msgid "ciReport|Download patch"
-msgstr ""
+msgstr "Завантажити патч"
msgid "ciReport|Dynamic Application Security Testing (DAST) detects known vulnerabilities in your web application."
msgstr "Динамічне тестування безпеки застосунків (DAST) виявляє відомі вразливості у вашому веб-застосунку."
@@ -11247,9 +11906,15 @@ msgstr "Виправлено:"
msgid "ciReport|Identifiers"
msgstr "Ідентифікатори"
+msgid "ciReport|Implement this solution by creating a merge request"
+msgstr ""
+
msgid "ciReport|Instances"
msgstr "Інстанси"
+msgid "ciReport|Investigate this vulnerability by creating an issue"
+msgstr ""
+
msgid "ciReport|Learn more about interacting with security reports (Alpha)."
msgstr "Дізнатися більше про взаємодію з звітами безпеки (Альфа)."
@@ -11297,9 +11962,6 @@ msgstr "Немає змін у показниках продуктивності
msgid "ciReport|Performance metrics"
msgstr "Показники продуктивності"
-msgid "ciReport|Revert dismissal"
-msgstr "Відмінити відхилення"
-
msgid "ciReport|SAST"
msgstr "SAST"
@@ -11321,6 +11983,9 @@ msgstr "Статичне тестування безпеки застосунк
msgid "ciReport|There was an error creating the issue. Please try again."
msgstr "Помилка при створенні задачі. Будь ласка спробуйте знову."
+msgid "ciReport|There was an error creating the merge request. Please try again."
+msgstr ""
+
msgid "ciReport|There was an error dismissing the vulnerability. Please try again."
msgstr "Помилка при відхиленні вразливості. Будь ласка, спробуйте знову."
@@ -11339,6 +12004,9 @@ msgstr "Помилка при завантаженні звіту по скан
msgid "ciReport|There was an error reverting the dismissal. Please try again."
msgstr "Помилка при відміні відхилення. Будь ласка, спробуйте знову."
+msgid "ciReport|Undo dismiss"
+msgstr ""
+
msgid "ciReport|Upgrade %{name} from %{version} to %{fixed}."
msgstr "Оновити %{name} з %{version} до %{fixed}."
@@ -11359,7 +12027,7 @@ msgid "command line instructions"
msgstr "інструкції для командного рядка"
msgid "commented on %{link_to_project}"
-msgstr ""
+msgstr "прокоментовано в %{link_to_project}"
msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue."
msgstr "Ви вимикаєте конфіденційність. Це означає, що <strong>будь-хто</strong> зможе бачити і залишати коментарі для цієї задачі."
@@ -11384,7 +12052,7 @@ msgstr[2] "днів"
msgstr[3] "днів"
msgid "deleted"
-msgstr ""
+msgstr "видалено"
msgid "deploy token"
msgstr "токен для розгортання"
@@ -11394,10 +12062,10 @@ msgstr "вимкнено"
msgid "discussion resolved"
msgid_plural "discussions resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "обговорення завершено"
+msgstr[1] "обговорення завершено"
+msgstr[2] "обговорень завершено"
+msgstr[3] "обговорень завершено"
msgid "done"
msgstr "готово"
@@ -11413,13 +12081,13 @@ msgid "enabled"
msgstr "увімкнено"
msgid "epic"
-msgstr ""
+msgstr "епік"
msgid "error"
-msgstr ""
+msgstr "помилка"
msgid "error code:"
-msgstr ""
+msgstr "код помилки:"
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
msgstr "%{slash_command} перезаписує запланований час останнім значенням."
@@ -11431,7 +12099,7 @@ msgid "from"
msgstr "від"
msgid "group"
-msgstr ""
+msgstr "група"
msgid "help"
msgstr "допомога"
@@ -11439,9 +12107,6 @@ msgstr "допомога"
msgid "here"
msgstr "тут"
-msgid "http://<sentry-host>/api/0/projects/{organization_slug}/{project_slug}/"
-msgstr ""
-
msgid "https://your-bitbucket-server"
msgstr "https://your-bitbucket-server"
@@ -11455,10 +12120,10 @@ msgid "importing"
msgstr "імпорт"
msgid "in group %{link_to_group}"
-msgstr ""
+msgstr "в групі %{link_to_group}"
msgid "in project %{link_to_project}"
-msgstr ""
+msgstr "в проекті %{link_to_project}"
msgid "index"
msgstr ""
@@ -11483,7 +12148,7 @@ msgid "is out of the hierarchy of the Group owning the template"
msgstr ""
msgid "issue"
-msgstr ""
+msgstr "задача"
msgid "issue boards"
msgstr "дошки обговорення задач"
@@ -11492,13 +12157,13 @@ msgid "it is stored externally"
msgstr ""
msgid "it is stored in LFS"
-msgstr ""
+msgstr "зберігається в LFS"
msgid "it is too large"
msgstr ""
msgid "latest"
-msgstr ""
+msgstr "останній"
msgid "latest deployment"
msgstr "останнє розгортання"
@@ -11523,19 +12188,22 @@ msgstr[2] "запитів на злиття"
msgstr[3] "запитів на злиття"
msgid "missing"
-msgstr ""
+msgstr "відсутні"
msgid "mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}."
+msgstr "%{commitCount} і %{mergeCommitCount} буде додано до %{targetBranch}."
+
+msgid "mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}."
msgstr ""
msgid "mrWidgetCommitsAdded|1 merge commit"
-msgstr ""
+msgstr "1 коміт-злиття"
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr "Будь ласка відновіть її або використовуйте іншу %{missingBranchName} гілку"
msgid "mrWidget|%{link_start}Learn more about resolving conflicts%{link_end}"
-msgstr ""
+msgstr "%{link_start}Докладніше про вирішення конфліктів%{link_end}"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
msgstr "Використання %{metricsLinkStart} пам’яті %{metricsLinkEnd} %{emphasisStart} впало %{emphasisEnd} з %{memoryFrom}Мб до %{memoryTo}Мб"
@@ -11553,10 +12221,10 @@ msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr "Дозволяє коміти від учасників, які можуть зливати до цільової гілки"
msgid "mrWidget|An error occurred while removing your approval."
-msgstr ""
+msgstr "Під час видалення вашого затвердження сталася помилка."
msgid "mrWidget|An error occurred while retrieving approval data for this merge request."
-msgstr ""
+msgstr "Помилка при отриманні даних про затвердження для цього запиту на злиття."
msgid "mrWidget|An error occurred while submitting your approval."
msgstr "Помилка при обробці вашого затвердження."
@@ -11564,6 +12232,9 @@ msgstr "Помилка при обробці вашого затвердженн
msgid "mrWidget|Approve"
msgstr "Затвердити"
+msgid "mrWidget|Approve additionally"
+msgstr ""
+
msgid "mrWidget|Approved by"
msgstr "Затверджено"
@@ -11595,7 +12266,7 @@ msgid "mrWidget|Create an issue to resolve them later"
msgstr "Створіть задачу, щоб вирішити їх пізніше"
msgid "mrWidget|Delete source branch"
-msgstr ""
+msgstr "Видалити гілку-джерело"
msgid "mrWidget|Deployment statistics are not available currently"
msgstr "Статистика розгортання наразі недоступна"
@@ -11636,17 +12307,20 @@ msgstr "Злити локально"
msgid "mrWidget|Merge request approved"
msgstr "Запит на злиття затверджено"
+msgid "mrWidget|Merge request approved."
+msgstr ""
+
msgid "mrWidget|Merge request approved; you can approve additionally"
msgstr "Запит на злиття було затверджено; Ви можете затвердити додатково"
msgid "mrWidget|Merged by"
msgstr "Злито"
-msgid "mrWidget|No Approval required"
-msgstr "Затвердження не потрібне"
+msgid "mrWidget|No approval required"
+msgstr ""
-msgid "mrWidget|No Approval required; you can still approve"
-msgstr "Затвердження не є обов’язковим; але ви все одно можете це зробити"
+msgid "mrWidget|No approval required; you can still approve"
+msgstr ""
msgid "mrWidget|Open in Web IDE"
msgstr "Відкрити у Web IDE"
@@ -11701,6 +12375,9 @@ msgstr "Анулювати"
msgid "mrWidget|Revert this merge request in a new merge request"
msgstr "Анулювати цей запит на злиття за допомогою нового запиту на злиття"
+msgid "mrWidget|Revoke approval"
+msgstr ""
+
msgid "mrWidget|Set by"
msgstr "Встановлено"
@@ -11720,19 +12397,19 @@ msgid "mrWidget|The source branch HEAD has recently changed. Please reload the p
msgstr "HEAD гілки-джерела нещодавно було змінено. Будь ласка оновіть сторінку і перегляньте зміни перед злиттям"
msgid "mrWidget|The source branch has been deleted"
-msgstr ""
+msgstr "Гілку-джерело видалено"
msgid "mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch"
msgstr "Гілка-джерело на %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} цільової гілки"
msgid "mrWidget|The source branch is being deleted"
-msgstr ""
+msgstr "Гілка-джерело в процесі видалення"
msgid "mrWidget|The source branch will be deleted"
-msgstr ""
+msgstr "Гілку-джерело буде видалено"
msgid "mrWidget|The source branch will not be deleted"
-msgstr ""
+msgstr "Гілку-джерело не буде видалено"
msgid "mrWidget|There are merge conflicts"
msgstr "існують конфлікти при злитті"
@@ -11741,7 +12418,7 @@ msgid "mrWidget|There are unresolved discussions. Please resolve these discussio
msgstr "Присутні незавершені обговорення. Будь ласка завершіть їх"
msgid "mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected."
-msgstr ""
+msgstr "Ця функція зливає зміни з цільової гілки до гілки-джерела. Її не можна використовувати, оскільки гілка-джерело є захищеною."
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr "Відбулася помилка при автоматичному злитті цього запиту"
@@ -11756,7 +12433,7 @@ msgid "mrWidget|You are not allowed to edit this project directly. Please fork t
msgstr "Ви не можете безпосередньо редагувати цей проект. Будь ласка, зробіть форк, щоб внести зміни."
msgid "mrWidget|You can delete the source branch now"
-msgstr ""
+msgstr "Тепер ви можете видалити гілку-джерело"
msgid "mrWidget|You can merge this merge request manually using the"
msgstr "Ви можете прийняти цей запит на злиття вручну за допомогою"
@@ -11780,19 +12457,19 @@ msgid "new merge request"
msgstr "Новий запит на злиття"
msgid "none"
-msgstr ""
+msgstr "немає"
msgid "notification emails"
msgstr "Повідомлення електронною поштою"
msgid "nounSeries|%{firstItem} and %{lastItem}"
-msgstr ""
+msgstr "%{firstItem} і %{lastItem}"
msgid "nounSeries|%{item}, %{nextItem}"
-msgstr ""
+msgstr "%{item}, %{nextItem}"
msgid "nounSeries|%{item}, and %{lastItem}"
-msgstr ""
+msgstr "%{item}, і %{lastItem}"
msgid "or"
msgstr "або"
@@ -11818,7 +12495,7 @@ msgid "personal access token"
msgstr "особистий токен доступу"
msgid "private"
-msgstr ""
+msgstr "приватний"
msgid "private key does not match certificate."
msgstr "приватний ключ не відповідає сертифікату."
@@ -11831,10 +12508,10 @@ msgstr[2] "проектів"
msgstr[3] "проектів"
msgid "quick actions"
-msgstr ""
+msgstr "швидкі дії"
msgid "register"
-msgstr ""
+msgstr "зареєструватися"
msgid "remaining"
msgstr "залишилось"
@@ -11859,16 +12536,19 @@ msgstr[2] "відповідей"
msgstr[3] "відповідей"
msgid "score"
+msgstr "результат"
+
+msgid "security Reports|There was an error creating the merge request"
msgstr ""
msgid "should be higher than %{access} inherited membership from group %{group_name}"
-msgstr ""
+msgstr "має бути вищим за %{access}, успадкованого з групи %{group_name}"
msgid "show less"
-msgstr ""
+msgstr "показати менше"
msgid "sign in"
-msgstr ""
+msgstr "увійти"
msgid "source"
msgstr "джерело"
@@ -11883,13 +12563,13 @@ msgid "started"
msgstr "розпочато"
msgid "stuck"
-msgstr ""
+msgstr "заблоковано"
msgid "syntax is correct"
-msgstr ""
+msgstr "синтаксис правильний"
msgid "syntax is incorrect"
-msgstr ""
+msgstr "синтаксис неправильний"
msgid "this document"
msgstr "цей документ"
@@ -11898,10 +12578,10 @@ msgid "to help your contributors communicate effectively!"
msgstr "щоб допомогти вашим контриб’юторам ефективно спілкуватися!"
msgid "triggered"
-msgstr ""
+msgstr "запущено"
msgid "updated"
-msgstr ""
+msgstr "оновлено"
msgid "username"
msgstr "ім'я користувача"
diff --git a/package.json b/package.json
index 7cdb11dca14..7d14ccc5a5e 100644
--- a/package.json
+++ b/package.json
@@ -30,9 +30,10 @@
"@babel/preset-env": "^7.3.1",
"@gitlab/csslab": "^1.8.0",
"@gitlab/svgs": "^1.54.0",
- "@gitlab/ui": "^2.2.2",
+ "@gitlab/ui": "^2.2.3",
"apollo-boost": "^0.3.1",
"apollo-client": "^2.5.1",
+ "at.js": "^1.5.4",
"autosize": "^4.0.0",
"axios": "^0.17.1",
"babel-loader": "^8.0.5",
@@ -75,6 +76,7 @@
"jest-transform-graphql": "^2.1.0",
"jquery": "^3.2.1",
"jquery-ujs": "1.2.2",
+ "jquery.caret": "^0.3.1",
"jquery.waitforimages": "^2.2.0",
"js-cookie": "^2.1.3",
"jszip": "^3.1.3",
@@ -169,7 +171,7 @@
"nodemon": "^1.18.9",
"pixelmatch": "^4.0.2",
"postcss": "^7.0.14",
- "prettier": "1.16.1",
+ "prettier": "1.16.4",
"stylelint": "^9.10.1",
"stylelint-config-recommended": "^2.1.0",
"stylelint-scss": "^3.5.3",
diff --git a/qa/qa.rb b/qa/qa.rb
index 2b3ffabbbaa..ec8aef31e48 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -61,6 +61,7 @@ module QA
autoload :File, 'qa/resource/file'
autoload :Fork, 'qa/resource/fork'
autoload :SSHKey, 'qa/resource/ssh_key'
+ autoload :Snippet, 'qa/resource/snippet'
module Events
autoload :Base, 'qa/resource/events/base'
@@ -142,6 +143,12 @@ module QA
module Dashboard
autoload :Projects, 'qa/page/dashboard/projects'
autoload :Groups, 'qa/page/dashboard/groups'
+
+ module Snippet
+ autoload :New, 'qa/page/dashboard/snippet/new'
+ autoload :Index, 'qa/page/dashboard/snippet/index'
+ autoload :Show, 'qa/page/dashboard/snippet/show'
+ end
end
module Group
@@ -342,6 +349,10 @@ module QA
module Specs
autoload :Config, 'qa/specs/config'
autoload :Runner, 'qa/specs/runner'
+
+ module Helpers
+ autoload :Quarantine, 'qa/specs/helpers/quarantine'
+ end
end
##
diff --git a/qa/qa/page/component/select2.rb b/qa/qa/page/component/select2.rb
index 98bcb96b92c..3c3461d6b20 100644
--- a/qa/qa/page/component/select2.rb
+++ b/qa/qa/page/component/select2.rb
@@ -3,7 +3,7 @@ module QA
module Component
module Select2
def select_item(item_text)
- find('.select2-result-label', text: item_text).click
+ find('.select2-result-label', text: item_text, match: :prefer_exact).click
end
def clear_current_selection_if_present
diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb
index 0f434577b3b..271c5456efe 100644
--- a/qa/qa/page/dashboard/projects.rb
+++ b/qa/qa/page/dashboard/projects.rb
@@ -2,8 +2,6 @@ module QA
module Page
module Dashboard
class Projects < Page::Base
- view 'app/views/dashboard/projects/index.html.haml'
-
view 'app/views/shared/projects/_search_form.html.haml' do
element :form_filter_by_name, /form_tag.+id: 'project-filter-form'/ # rubocop:disable QA/ElementWithPattern
end
diff --git a/qa/qa/page/dashboard/snippet/index.rb b/qa/qa/page/dashboard/snippet/index.rb
new file mode 100644
index 00000000000..1f467fda9e1
--- /dev/null
+++ b/qa/qa/page/dashboard/snippet/index.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Dashboard
+ module Snippet
+ class Index < Page::Base
+ view 'app/views/layouts/header/_new_dropdown.haml' do
+ element :new_menu_toggle
+ element :global_new_snippet_link
+ end
+
+ def go_to_new_snippet_page
+ click_element :new_menu_toggle
+ click_element :global_new_snippet_link
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/dashboard/snippet/new.rb b/qa/qa/page/dashboard/snippet/new.rb
new file mode 100644
index 00000000000..a637b869d2f
--- /dev/null
+++ b/qa/qa/page/dashboard/snippet/new.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Dashboard
+ module Snippet
+ class New < Page::Base
+ view 'app/views/shared/form_elements/_description.html.haml' do
+ element :issuable_form_description
+ end
+
+ view 'app/views/shared/snippets/_form.html.haml' do
+ element :snippet_title
+ element :snippet_file_name
+ element :create_snippet_button
+ end
+
+ def fill_title(title)
+ fill_element :snippet_title, title
+ end
+
+ def fill_description(description)
+ fill_element :issuable_form_description, description
+ end
+
+ def set_visibility(visibility)
+ choose visibility
+ end
+
+ def fill_file_name(name)
+ finished_loading?
+ fill_element :snippet_file_name, name
+ end
+
+ def fill_file_content(content)
+ finished_loading?
+ text_area.set content
+ end
+
+ def create_snippet
+ click_element :create_snippet_button
+ end
+
+ private
+
+ def text_area
+ find('#editor>textarea', visible: false)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/dashboard/snippet/show.rb b/qa/qa/page/dashboard/snippet/show.rb
new file mode 100644
index 00000000000..a75ea63eca7
--- /dev/null
+++ b/qa/qa/page/dashboard/snippet/show.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Dashboard
+ module Snippet
+ class Show < Page::Base
+ view 'app/views/shared/snippets/_header.html.haml' do
+ element :snippet_title
+ element :snippet_description
+ element :embed_type
+ element :snippet_box
+ end
+
+ view 'app/views/projects/blob/_header_content.html.haml' do
+ element :file_title_name
+ end
+
+ view 'app/views/shared/_file_highlight.html.haml' do
+ element :file_content
+ end
+
+ def has_snippet_title?(snippet_title)
+ within_element(:snippet_title) do
+ has_text?(snippet_title)
+ end
+ end
+
+ def has_snippet_description?(snippet_description)
+ within_element(:snippet_description) do
+ has_text?(snippet_description)
+ end
+ end
+
+ def has_embed_type?(embed_type)
+ within_element(:embed_type) do
+ has_text?(embed_type)
+ end
+ end
+
+ def has_visibility_type?(visibility_type)
+ within_element(:snippet_box) do
+ has_text?(visibility_type)
+ end
+ end
+
+ def has_file_name?(file_name)
+ within_element(:file_title_name) do
+ has_text?(file_name)
+ end
+ end
+
+ def has_file_content?(file_content)
+ finished_loading?
+ within_element(:file_content) do
+ has_text?(file_content)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 55500e831c6..1b3445b0064 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -19,6 +19,7 @@ module QA
element :admin_area_link
element :projects_dropdown
element :groups_dropdown
+ element :snippets_link
end
view 'app/views/layouts/nav/projects_dropdown/_show.html.haml' do
@@ -66,6 +67,10 @@ module QA
end
end
+ def go_to_snippets
+ click_element :snippets_link
+ end
+
def has_personal_area?(wait: Capybara.default_max_wait_time)
has_element?(:user_avatar, wait: wait)
end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index eabeae1acc4..552b2293115 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -24,11 +24,14 @@ module QA
end
def choose_test_namespace
+ choose_namespace(Runtime::Namespace.path)
+ end
+
+ def choose_namespace(namespace)
retry_on_exception do
click_body
click_element :project_namespace_select
-
- search_and_select(Runtime::Namespace.path)
+ search_and_select(namespace)
end
end
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
index 2b6c01888d5..ff7cc04e352 100644
--- a/qa/qa/page/project/web_ide/edit.rb
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -43,11 +43,9 @@ module QA
def create_new_file_from_template(file_name, template)
click_element :new_file
within_element(:template_list) do
- begin
- click_on file_name
- rescue Capybara::ElementNotFound
- raise ElementNotFound, %Q(Couldn't find file template named "#{file_name}". Please confirm that it is a valid option.)
- end
+ click_on file_name
+ rescue Capybara::ElementNotFound
+ raise ElementNotFound, %Q(Couldn't find file template named "#{file_name}". Please confirm that it is a valid option.)
end
wait(reload: false) do
diff --git a/qa/qa/resource/snippet.rb b/qa/qa/resource/snippet.rb
new file mode 100644
index 00000000000..1478f197570
--- /dev/null
+++ b/qa/qa/resource/snippet.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Snippet < Base
+ attr_accessor :title, :description, :file_content, :visibility, :file_name
+
+ def initialize
+ @title = 'New snippet title'
+ @description = 'The snippet description'
+ @visibility = 'Public'
+ @file_content = 'The snippet content'
+ @file_name = 'New snippet file name'
+ end
+
+ def fabricate!
+ Page::Dashboard::Snippet::Index.perform(&:go_to_new_snippet_page)
+
+ Page::Dashboard::Snippet::New.perform do |page|
+ page.fill_title(@title)
+ page.fill_description(@description)
+ page.set_visibility(@visibility)
+ page.fill_file_name(@file_name)
+ page.fill_file_content(@file_content)
+ page.create_snippet
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
index 4070a225260..d8609aa037a 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
@@ -19,7 +19,7 @@ module QA
page.add_member(user.username)
end
- expect(page).to have_content("#{user.name} @#{user.username} Given access")
+ expect(page).to have_content(/#{user.name} (. )?@#{user.username} Given access/)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
new file mode 100644
index 00000000000..ab53dff464e
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create', :smoke do
+ describe 'Snippet creation' do
+ it 'User creates a snippet' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ Page::Main::Menu.perform(&:go_to_snippets)
+
+ Resource::Snippet.fabricate_via_browser_ui! do |snippet|
+ snippet.title = 'Snippet title'
+ snippet.description = 'Snippet description'
+ snippet.visibility = 'Public'
+ snippet.file_name = 'New snippet file name'
+ snippet.file_content = 'Snippet file text'
+ end
+
+ Page::Dashboard::Snippet::Show.perform do |snippet|
+ expect(snippet).to have_snippet_title('Snippet title')
+ expect(snippet).to have_snippet_description('Snippet description')
+ expect(snippet).to have_embed_type('Embed')
+ expect(snippet).to have_visibility_type('Public')
+ expect(snippet).to have_file_name('New snippet file name')
+ expect(snippet).to have_file_content('Snippet file text')
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
index f176ec31abd..66cd712afb0 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module QA
- context 'Create' do
+ # Failure issue: https://gitlab.com/gitlab-org/quality/staging/issues/46
+ context 'Create', :quarantine do
describe 'Web IDE file templates' do
include Runtime::Fixtures
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index 4212a2b0392..16aca7c0e22 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -3,16 +3,24 @@
require 'pathname'
module QA
- # Transient failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/68
context 'Configure' do
def login
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
end
+ # Transient failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/68
describe 'Auto DevOps support', :orchestrated, :kubernetes, :quarantine do
context 'when rbac is enabled' do
before(:all) do
+ @cluster = Service::KubernetesCluster.new.create!
+ end
+
+ after(:all) do
+ @cluster&.remove!
+ end
+
+ it 'runs auto devops' do
login
@project = Resource::Project.fabricate! do |p|
@@ -28,8 +36,14 @@ module QA
resource.value = '1'
end
- # Create and connect K8s cluster
- @cluster = Service::KubernetesCluster.new.create!
+ # Set an application secret CI variable (prefixed with K8S_SECRET_)
+ Resource::CiVariable.fabricate! do |resource|
+ resource.project = @project
+ resource.key = 'K8S_SECRET_OPTIONAL_MESSAGE'
+ resource.value = 'You can see this application secret'
+ end
+
+ # Connect K8s cluster
Resource::KubernetesCluster.fabricate! do |cluster|
cluster.project = @project
cluster.cluster = @cluster
@@ -47,14 +61,7 @@ module QA
.join('../../../../../fixtures/auto_devops_rack')
push.commit_message = 'Create Auto DevOps compatible rack application'
end
- end
- after(:all) do
- @cluster&.remove!
- end
-
- it 'runs auto devops' do
- @project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:go_to_latest_pipeline)
@@ -92,66 +99,6 @@ module QA
Page::Project::Operations::Environments::Show.perform do |show|
show.view_deployment do
expect(page).to have_content('Hello World!')
- end
- end
- end
-
- it 'user sets application secret variable and Auto DevOps passes it to container' do
- # Set an application secret CI variable (prefixed with K8S_SECRET_)
- Resource::CiVariable.fabricate! do |resource|
- resource.project = @project
- resource.key = 'K8S_SECRET_OPTIONAL_MESSAGE'
- resource.value = 'You can see this application secret'
- end
-
- # Our current Auto DevOps implementation won't update the production
- # app if we only update a CI variable with no code change.
- #
- # Workaround: push new code and use the resultant pipeline.
- Resource::Repository::ProjectPush.fabricate! do |push|
- push.project = @project
- push.commit_message = 'Force a Deployment change by pushing new code'
- push.file_name = 'new_file.txt'
- push.file_content = 'new file contents'
- end
-
- Page::Project::Menu.perform(&:click_ci_cd_pipelines)
- Page::Project::Pipeline::Index.perform(&:go_to_latest_pipeline)
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.go_to_job('build')
- end
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 600)
-
- job.click_element(:pipeline_path)
- end
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.go_to_job('test')
- end
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 600)
-
- job.click_element(:pipeline_path)
- end
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.go_to_job('production')
- end
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 1200)
- end
-
- Page::Project::Menu.perform(&:click_operations_environments)
-
- Page::Project::Operations::Environments::Index.perform do |index|
- index.go_to_environment('production')
- end
-
- Page::Project::Operations::Environments::Show.perform do |show|
- show.view_deployment do
- expect(page).to have_content('Hello World!')
expect(page).to have_content('You can see this application secret')
end
end
@@ -159,7 +106,8 @@ module QA
end
end
- describe 'Auto DevOps', :smoke do
+ # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/87
+ describe 'Auto DevOps', :smoke, :quarantine do
it 'enables AutoDevOps by default' do
login
diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb
new file mode 100644
index 00000000000..52cb05fcd13
--- /dev/null
+++ b/qa/qa/specs/helpers/quarantine.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'rspec/core'
+
+module QA::Specs::Helpers
+ module Quarantine
+ include RSpec::Core::Pending
+
+ extend self
+
+ def configure_rspec
+ RSpec.configure do |config|
+ config.before(:context, :quarantine) do
+ Quarantine.skip_or_run_quarantined_contexts(config.inclusion_filter.rules, self.class)
+ end
+
+ config.before do |example|
+ Quarantine.skip_or_run_quarantined_tests_or_contexts(config.inclusion_filter.rules, example)
+ end
+ end
+ end
+
+ # Skip tests in quarantine unless we explicitly focus on them.
+ def skip_or_run_quarantined_tests_or_contexts(filters, example)
+ if filters.key?(:quarantine)
+ included_filters = filters_other_than_quarantine(filters)
+
+ # If :quarantine is focused, skip the test/context unless its metadata
+ # includes quarantine and any other filters
+ # E.g., Suppose a test is tagged :smoke and :quarantine, and another is tagged
+ # :ldap and :quarantine. If we wanted to run just quarantined smoke tests
+ # using `--tag quarantine --tag smoke`, without this check we'd end up
+ # running that ldap test as well because of the :quarantine metadata.
+ # We could use an exclusion filter, but this way the test report will list
+ # the quarantined tests when they're not run so that we're aware of them
+ skip("Only running tests tagged with :quarantine and any of #{included_filters.keys}") if should_skip_when_focused?(example.metadata, included_filters)
+ else
+ skip('In quarantine') if example.metadata.key?(:quarantine)
+ end
+ end
+
+ # Skip the entire context if a context is quarantined. This avoids running
+ # before blocks unnecessarily.
+ def skip_or_run_quarantined_contexts(filters, example)
+ return unless example.metadata.key?(:quarantine)
+
+ skip_or_run_quarantined_tests_or_contexts(filters, example)
+ end
+
+ def filters_other_than_quarantine(filter)
+ filter.reject { |key, _| key == :quarantine }
+ end
+
+ # Checks if a test or context should be skipped.
+ #
+ # Returns true if
+ # - the metadata does not includes the :quarantine tag
+ # or if
+ # - the metadata includes the :quarantine tag
+ # - and the filter includes other tags that aren't in the metadata
+ def should_skip_when_focused?(metadata, included_filters)
+ return true unless metadata.key?(:quarantine)
+ return false if included_filters.empty?
+
+ (metadata.keys & included_filters.keys).empty?
+ end
+ end
+end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index cbdd6e881b1..be13c3fb683 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -6,16 +6,10 @@ require 'rspec/retry'
end
RSpec.configure do |config|
- config.before(:context) do
- if self.class.metadata.keys.include?(:quarantine)
- skip_or_run_quarantined_tests(self.class.metadata.keys, config.inclusion_filter.rules.keys)
- end
- end
+ QA::Specs::Helpers::Quarantine.configure_rspec
config.before do |example|
QA::Runtime::Logger.debug("Starting test: #{example.full_description}") if QA::Runtime::Env.debug?
-
- skip_or_run_quarantined_tests(example.metadata.keys, config.inclusion_filter.rules.keys)
end
config.expect_with :rspec do |expectations|
@@ -44,42 +38,3 @@ RSpec.configure do |config|
example.run_with_retry retry: retry_times
end
end
-
-# Skip tests in quarantine unless we explicitly focus on them.
-# Skip the entire context if a context is tagged. This avoids running before
-# blocks unnecessarily.
-# If quarantine is focussed, skip tests/contexts that have other metadata
-# unless they're also focussed. This lets us run quarantined tests in a
-# particular category without running tests in other categories.
-# E.g., if a test is tagged 'smoke' and 'quarantine', and another is tagged
-# 'ldap' and 'quarantine', if we wanted to run just quarantined smoke tests
-# using `--tag quarantine --tag smoke`, without this check we'd end up
-# running that ldap test as well.
-# We could use an exclusion filter, but this way the test report will list
-# the quarantined tests when they're not run so that we're aware of them
-def skip_or_run_quarantined_tests(metadata_keys, filter_keys)
- included_filters = filters_other_than_quarantine(filter_keys)
-
- if filter_keys.include?(:quarantine)
- skip("Only running tests tagged with :quarantine and any of #{included_filters}") unless quarantine_and_optional_other_tag?(metadata_keys, included_filters)
- else
- skip('In quarantine') if metadata_keys.include?(:quarantine)
- end
-end
-
-def filters_other_than_quarantine(filter_keys)
- filter_keys.reject { |key| key == :quarantine }
-end
-
-# Checks if a test has the 'quarantine' tag and other tags in the inclusion filter.
-#
-# Returns true if
-# - the metadata includes the quarantine tag
-# - and the metadata and inclusion filter both have any other tag
-# - or no other tags are in the inclusion filter
-def quarantine_and_optional_other_tag?(metadata_keys, included_filters)
- return false unless metadata_keys.include? :quarantine
- return true if included_filters.empty?
-
- included_filters.any? { |key| metadata_keys.include? key }
-end
diff --git a/qa/spec/spec_helper_spec.rb b/qa/spec/spec_helper_spec.rb
deleted file mode 100644
index 27ec1ec80fe..00000000000
--- a/qa/spec/spec_helper_spec.rb
+++ /dev/null
@@ -1,355 +0,0 @@
-# frozen_string_literal: true
-
-describe 'rspec config tests' do
- let(:group) do
- RSpec.describe do
- shared_examples 'passing tests' do
- example 'not in quarantine' do
- end
- example 'in quarantine', :quarantine do
- end
- end
-
- context 'default' do
- it_behaves_like 'passing tests'
- end
-
- context 'foo', :foo do
- it_behaves_like 'passing tests'
- end
-
- context 'quarantine', :quarantine do
- it_behaves_like 'passing tests'
- end
-
- context 'bar quarantine', :bar, :quarantine do
- it_behaves_like 'passing tests'
- end
- end
- end
-
- let(:group_2) do
- RSpec.describe do
- before(:all) do
- @expectations = [1, 2, 3]
- end
-
- example 'not in quarantine' do
- expect(@expectations.shift).to be(3)
- end
-
- example 'in quarantine', :quarantine do
- expect(@expectations.shift).to be(3)
- end
- end
- end
-
- context 'with no tags focussed' do
- before do
- group.run
- end
-
- context 'in a context tagged :foo' do
- it 'skips tests in quarantine' do
- context = group.children.find { |c| c.description == "foo" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to eq(2)
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- expect(ex.execution_result.pending_message).to eq('In quarantine')
- end
- end
-
- context 'in an untagged context' do
- it 'skips tests in quarantine' do
- context = group.children.find { |c| c.description == "default" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to eq(2)
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- expect(ex.execution_result.pending_message).to eq('In quarantine')
- end
- end
-
- context 'in a context tagged :quarantine' do
- it 'skips all tests' do
- context = group.children.find { |c| c.description == "quarantine" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to eq(2)
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- expect(ex.execution_result.pending_message).to eq('In quarantine')
- end
- end
- end
-
- context 'with :quarantine focussed' do
- before do
- RSpec.configure do |config|
- config.inclusion_filter = :quarantine
- end
-
- group.run
- end
- after do
- RSpec.configure do |config|
- config.inclusion_filter.clear
- end
- end
-
- context 'in an untagged context' do
- it 'only runs quarantined tests' do
- context = group.children.find { |c| c.description == "default" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to be(1)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
- end
- end
-
- context 'in a context tagged :foo' do
- it 'only runs quarantined tests' do
- context = group.children.find { |c| c.description == "foo" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to be(1)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
- end
- end
-
- context 'in a context tagged :quarantine' do
- it 'runs all tests' do
- context = group.children.find { |c| c.description == "quarantine" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to be(2)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
- end
- end
- end
-
- context 'with a non-quarantine tag (:foo) focussed' do
- before do
- RSpec.configure do |config|
- config.inclusion_filter = :foo
- end
-
- group.run
- end
- after do
- RSpec.configure do |config|
- config.inclusion_filter.clear
- end
- end
-
- context 'in an untagged context' do
- it 'runs no tests' do
- context = group.children.find { |c| c.description == "default" }
- expect(context.descendant_filtered_examples.count).to eq(0)
- end
- end
-
- context 'in a context tagged :foo' do
- it 'skips quarantined tests' do
- context = group.children.find { |c| c.description == "foo" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to be(2)
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- expect(ex.execution_result.pending_message).to eq('In quarantine')
- end
- end
-
- context 'in a context tagged :quarantine' do
- it 'runs no tests' do
- context = group.children.find { |c| c.description == "quarantine" }
- expect(context.descendant_filtered_examples.count).to eq(0)
- end
- end
- end
-
- context 'with :quarantine and a non-quarantine tag (:foo) focussed' do
- before do
- RSpec.configure do |config|
- config.inclusion_filter = { quarantine: true, foo: true }
- end
-
- group.run
- end
- after do
- RSpec.configure do |config|
- config.inclusion_filter.clear
- end
- end
-
- context 'in an untagged context' do
- it 'ignores untagged tests and skips tests even if in quarantine' do
- context = group.children.find { |c| c.description == "default" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to eq(1)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- end
- end
-
- context 'in a context tagged :foo' do
- it 'only runs quarantined tests' do
- context = group.children.find { |c| c.description == "foo" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to be(2)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- end
- end
-
- context 'in a context tagged :quarantine' do
- it 'skips all tests' do
- context = group.children.find { |c| c.description == "quarantine" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to be(2)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- end
- end
-
- context 'in a context tagged :bar and :quarantine' do
- it 'skips all tests' do
- context = group.children.find { |c| c.description == "quarantine" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to be(2)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- end
- end
- end
-
- context 'with :quarantine and multiple non-quarantine tags focussed' do
- before do
- RSpec.configure do |config|
- config.inclusion_filter = { bar: true, foo: true, quarantine: true }
- end
-
- group.run
- end
- after do
- RSpec.configure do |config|
- config.inclusion_filter.clear
- end
- end
-
- context 'in a context tagged :foo' do
- it 'only runs quarantined tests' do
- context = group.children.find { |c| c.description == "foo" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to be(2)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- expect(ex.execution_result.pending_message).to eq('Only running tests tagged with :quarantine and any of [:bar, :foo]')
- end
- end
-
- context 'in a context tagged :quarantine' do
- it 'skips all tests' do
- context = group.children.find { |c| c.description == "quarantine" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to be(2)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- expect(ex.execution_result.pending_message).to eq('Only running tests tagged with :quarantine and any of [:bar, :foo]')
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:pending)
- expect(ex.execution_result.pending_message).to eq('Only running tests tagged with :quarantine and any of [:bar, :foo]')
- end
- end
-
- context 'in a context tagged :bar and :quarantine' do
- it 'runs all tests' do
- context = group.children.find { |c| c.description == "bar quarantine" }
- examples = context.descendant_filtered_examples
- expect(examples.count).to be(2)
-
- ex = examples.find { |e| e.description == "in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
-
- ex = examples.find { |e| e.description == "not in quarantine" }
- expect(ex.execution_result.status).to eq(:passed)
- end
- end
- end
-
- context 'rspec retry' do
- context 'in an untagged context' do
- before do
- group_2.run
- end
-
- it 'should run example :retry times' do
- examples = group_2.descendant_filtered_examples
- ex = examples.find { |e| e.description == 'not in quarantine' }
- expect(ex.execution_result.status).to eq(:passed)
- end
- end
-
- context 'with :quarantine focussed' do
- before do
- RSpec.configure do |config|
- config.inclusion_filter = :quarantine
- end
- group_2.run
- end
-
- after do
- RSpec.configure do |config|
- config.inclusion_filter.clear
- end
- end
-
- it 'should run example once only' do
- examples = group_2.descendant_filtered_examples
- ex = examples.find { |e| e.description == 'in quarantine' }
- expect(ex.execution_result.status).to eq(:failed)
- end
- end
- end
-end
diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb
new file mode 100644
index 00000000000..78beda39b5e
--- /dev/null
+++ b/qa/spec/specs/helpers/quarantine_spec.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+require 'rspec/core/sandbox'
+
+# We need a reporter for internal tests that's different from the reporter for
+# external tests otherwise the results will be mixed up. We don't care about
+# most reporting, but we do want to know if a test fails
+class RaiseOnFailuresReporter < RSpec::Core::NullReporter
+ def self.example_failed(example)
+ raise example.exception
+ end
+end
+
+# We use an example group wrapper to prevent the state of internal tests
+# expanding into the global state
+# See: https://github.com/rspec/rspec-core/issues/2603
+def describe_successfully(*args, &describe_body)
+ example_group = RSpec.describe(*args, &describe_body)
+ ran_successfully = example_group.run RaiseOnFailuresReporter
+ expect(ran_successfully).to eq true
+ example_group
+end
+
+RSpec.configure do |c|
+ c.around do |ex|
+ RSpec::Core::Sandbox.sandboxed do |config|
+ # If there is an example-within-an-example, we want to make sure the inner example
+ # does not get a reference to the outer example (the real spec) if it calls
+ # something like `pending`
+ config.before(:context) { RSpec.current_example = nil }
+
+ config.color_mode = :off
+
+ # Load airborne again to avoid "undefined method `match_expected_default?'" errors
+ # that happen because a hook calls a method added via a custom RSpec setting
+ # that is removed when the RSpec configuration is sandboxed.
+ # If this needs to be changed (e.g., to load other libraries as well), see
+ # this discussion for alternative solutions:
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/25223#note_143392053
+ load 'airborne.rb'
+
+ ex.run
+ end
+ end
+end
+
+describe QA::Specs::Helpers::Quarantine do
+ describe '.skip_or_run_quarantined_contexts' do
+ context 'with no tag focused' do
+ before do
+ described_class.configure_rspec
+ end
+
+ it 'skips before hooks of quarantined contexts' do
+ executed_hooks = []
+
+ group = describe_successfully('quarantine', :quarantine) do
+ before(:all) do
+ executed_hooks << :before_all
+ end
+ before do
+ executed_hooks << :before
+ end
+ example {}
+ end
+
+ expect(executed_hooks).to eq []
+ expect(group.descendant_filtered_examples.first.execution_result.status).to eq(:pending)
+ expect(group.descendant_filtered_examples.first.execution_result.pending_message)
+ .to eq('In quarantine')
+ end
+
+ it 'executes before hooks of non-quarantined contexts' do
+ executed_hooks = []
+
+ group = describe_successfully do
+ before(:all) do
+ executed_hooks << :before_all
+ end
+ before do
+ executed_hooks << :before
+ end
+ example {}
+ end
+
+ expect(executed_hooks).to eq [:before_all, :before]
+ expect(group.descendant_filtered_examples.first.execution_result.status).to eq(:passed)
+ end
+ end
+
+ context 'with :quarantine focused' do
+ before do
+ described_class.configure_rspec
+ RSpec.configure do |c|
+ c.filter_run :quarantine
+ end
+ end
+
+ it 'executes before hooks of quarantined contexts' do
+ executed_hooks = []
+
+ group = describe_successfully('quarantine', :quarantine) do
+ before(:all) do
+ executed_hooks << :before_all
+ end
+ before do
+ executed_hooks << :before
+ end
+ example {}
+ end
+
+ expect(executed_hooks).to eq [:before_all, :before]
+ expect(group.descendant_filtered_examples.first.execution_result.status).to eq(:passed)
+ end
+
+ it 'skips before hooks of non-quarantined contexts' do
+ executed_hooks = []
+
+ group = describe_successfully do
+ before(:all) do
+ executed_hooks << :before_all
+ end
+ before do
+ executed_hooks << :before
+ end
+ example {}
+ end
+
+ expect(executed_hooks).to eq []
+ expect(group.descendant_filtered_examples.first).to be_nil
+ end
+ end
+ end
+
+ describe '.skip_or_run_quarantined_tests' do
+ context 'with no tag focused' do
+ before do
+ described_class.configure_rspec
+ end
+
+ it 'skips quarantined tests' do
+ group = describe_successfully do
+ it('is pending', :quarantine) {}
+ end
+
+ expect(group.examples.first.execution_result.status).to eq(:pending)
+ expect(group.examples.first.execution_result.pending_message)
+ .to eq('In quarantine')
+ end
+
+ it 'executes non-quarantined tests' do
+ group = describe_successfully do
+ example {}
+ end
+
+ expect(group.examples.first.execution_result.status).to eq(:passed)
+ end
+ end
+
+ context 'with :quarantine focused' do
+ before do
+ described_class.configure_rspec
+ RSpec.configure do |c|
+ c.filter_run :quarantine
+ end
+ end
+
+ it 'executes quarantined tests' do
+ group = describe_successfully do
+ it('passes', :quarantine) {}
+ end
+
+ expect(group.examples.first.execution_result.status).to eq(:passed)
+ end
+
+ it 'ignores non-quarantined tests' do
+ group = describe_successfully do
+ example {}
+ end
+
+ expect(group.examples.first.execution_result.status).to be_nil
+ end
+ end
+
+ context 'with a non-quarantine tag focused' do
+ before do
+ described_class.configure_rspec
+ RSpec.configure do |c|
+ c.filter_run :foo
+ end
+ end
+
+ it 'ignores non-quarantined non-focused tests' do
+ group = describe_successfully do
+ example {}
+ end
+
+ expect(group.examples.first.execution_result.status).to be_nil
+ end
+
+ it 'executes non-quarantined focused tests' do
+ group = describe_successfully do
+ it('passes', :foo) {}
+ end
+
+ expect(group.examples.first.execution_result.status).to be(:passed)
+ end
+
+ it 'ignores quarantined tests' do
+ group = describe_successfully do
+ it('is ignored', :quarantine) {}
+ end
+
+ expect(group.examples.first.execution_result.status).to be_nil
+ end
+
+ it 'skips quarantined focused tests' do
+ group = describe_successfully do
+ it('is pending', :quarantine, :foo) {}
+ end
+
+ expect(group.examples.first.execution_result.status).to be(:pending)
+ expect(group.examples.first.execution_result.pending_message)
+ .to eq('In quarantine')
+ end
+ end
+
+ context 'with :quarantine and non-quarantine tags focused' do
+ before do
+ described_class.configure_rspec
+ RSpec.configure do |c|
+ c.filter_run :foo, :bar, :quarantine
+ end
+ end
+
+ it 'ignores non-quarantined non-focused tests' do
+ group = describe_successfully do
+ example {}
+ end
+
+ expect(group.examples.first.execution_result.status).to be_nil
+ end
+
+ it 'skips non-quarantined focused tests' do
+ group = describe_successfully do
+ it('is pending', :foo) {}
+ end
+
+ expect(group.examples.first.execution_result.status).to be(:pending)
+ expect(group.examples.first.execution_result.pending_message)
+ .to eq('Only running tests tagged with :quarantine and any of [:bar, :foo]')
+ end
+
+ it 'skips quarantined non-focused tests' do
+ group = describe_successfully do
+ it('is pending', :quarantine) {}
+ end
+
+ expect(group.examples.first.execution_result.status).to be(:pending)
+ end
+
+ it 'executes quarantined focused tests' do
+ group = describe_successfully do
+ it('passes', :quarantine, :foo) {}
+ end
+
+ expect(group.examples.first.execution_result.status).to be(:passed)
+ end
+ end
+ end
+end
diff --git a/scripts/build_assets_image b/scripts/build_assets_image
index 9afada244c8..b659bd751b2 100755
--- a/scripts/build_assets_image
+++ b/scripts/build_assets_image
@@ -27,3 +27,9 @@ docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
docker push ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG}
docker push ${ASSETS_IMAGE_PATH}:${CI_COMMIT_SHA}
+# Also tag the image with GitLab version, if running on a tag pipeline, so
+# other projects can simply use that instead of computing the slug.
+if [ -n "$CI_COMMIT_TAG" ]; then
+ docker tag ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG} ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_NAME}
+ docker push ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_NAME}
+fi
diff --git a/scripts/frontend/postinstall.js b/scripts/frontend/postinstall.js
index 682039a41b3..94977e459e3 100644
--- a/scripts/frontend/postinstall.js
+++ b/scripts/frontend/postinstall.js
@@ -13,7 +13,7 @@ if (process.platform === 'darwin') {
ensure that it is supported by the fsevents library.
You can try installing again with \`${chalk.cyan('yarn install --force')}\`
- `)
+ `),
);
process.exit(1);
}
diff --git a/scripts/frontend/prettier.js b/scripts/frontend/prettier.js
index ffb09ea9779..bf0e98da139 100644
--- a/scripts/frontend/prettier.js
+++ b/scripts/frontend/prettier.js
@@ -32,7 +32,7 @@ let globDir = process.argv[3] || '';
if (globDir && globDir.charAt(globDir.length - 1) !== '/') globDir += '/';
console.log(
- `Loading all ${allFiles ? '' : 'staged '}files ${globDir ? `within ${globDir} ` : ''}...`
+ `Loading all ${allFiles ? '' : 'staged '}files ${globDir ? `within ${globDir} ` : ''}...`,
);
const globPatterns = matchExtensions.map(ext => `${globDir}**/*.${ext}`);
@@ -105,7 +105,7 @@ Promise.all(matchedFiles.map(checkFileWithPrettierConfig))
.then(() => {
const failAction = shouldSave ? 'fixed' : 'failed';
console.log(
- `\nSummary:\n ${matchedCount} files processed (${passedCount} passed, ${failedCount} ${failAction}, ${ignoredCount} ignored)\n`
+ `\nSummary:\n ${matchedCount} files processed (${passedCount} passed, ${failedCount} ${failAction}, ${ignoredCount} ignored)\n`,
);
if (didWarn) process.exit(1);
diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb
index dee4c2eba7e..b5d3facd18a 100644
--- a/scripts/gitaly_test.rb
+++ b/scripts/gitaly_test.rb
@@ -79,15 +79,13 @@ module GitalyTest
socket = read_socket_path
Integer(timeout / delay).times do
- begin
- UNIXSocket.new(socket)
- puts ' OK'
-
- return
- rescue Errno::ENOENT, Errno::ECONNREFUSED
- print '.'
- sleep delay
- end
+ UNIXSocket.new(socket)
+ puts ' OK'
+
+ return
+ rescue Errno::ENOENT, Errno::ECONNREFUSED
+ print '.'
+ sleep delay
end
puts ' FAILED'
diff --git a/scripts/insert-rspec-profiling-data b/scripts/insert-rspec-profiling-data
new file mode 100755
index 00000000000..10e337b9972
--- /dev/null
+++ b/scripts/insert-rspec-profiling-data
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+
+require 'csv'
+require 'rspec_profiling'
+require 'postgres-copy'
+
+module RspecProfiling
+ module Collectors
+ class PSQL
+ def establish_connection
+ # This disables the automatic creation of the database and
+ # table. In the future, we may want a way to specify the host of
+ # the database to connect so that we can call #install.
+ Result.establish_connection(results_url)
+ end
+
+ def prepared?
+ connection.data_source_exists?(table)
+ end
+
+ def results_url
+ ENV['RSPEC_PROFILING_POSTGRES_URL']
+ end
+
+ class Result < ActiveRecord::Base
+ acts_as_copy_target
+ end
+ end
+ end
+end
+
+def insert_data(path)
+ puts "#{Time.now} Inserting CI stats..."
+
+ collector = RspecProfiling::Collectors::PSQL.new
+ collector.install
+
+ files = Dir[File.join(path, "*.csv")]
+
+ files.each do |filename|
+ puts "#{Time.now} Inserting #{filename}..."
+ result = RspecProfiling::Collectors::PSQL::Result.copy_from(filename)
+ puts "#{Time.now} Inserted #{result.cmd_tuples} lines in #{filename}, DB response: #{result.cmd_status}"
+ end
+end
+
+insert_data('rspec_profiling') if ENV['RSPEC_PROFILING_POSTGRES_URL'].present?
diff --git a/scripts/merge-simplecov b/scripts/merge-simplecov
index 65f93f8830b..746be3317a7 100755
--- a/scripts/merge-simplecov
+++ b/scripts/merge-simplecov
@@ -12,11 +12,9 @@ module SimpleCov
def resultset_hashes
resultset_files.map do |path|
- begin
- JSON.parse(File.read(path))
- rescue
- {}
- end
+ JSON.parse(File.read(path))
+ rescue
+ {}
end
end
diff --git a/scripts/trigger-build b/scripts/trigger-build
index 9dbafffddfc..9c5fc3c76a5 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build
@@ -141,7 +141,7 @@ module Trigger
"GITLAB_#{edition}_VERSION" => ENV['CI_COMMIT_REF_NAME'],
"GITLAB_VERSION" => ENV['CI_COMMIT_REF_NAME'],
"GITLAB_TAG" => ENV['CI_COMMIT_TAG'],
- "GITLAB_ASSETS_TAG" => ENV['CI_COMMIT_REF_SLUG'],
+ "GITLAB_ASSETS_TAG" => ENV['CI_COMMIT_TAG'] ? ENV['CI_COMMIT_REF_NAME'] : ENV['CI_COMMIT_REF_SLUG'],
"#{edition}_PIPELINE" => 'true'
}
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 4458a7223bf..d8b75c5151e 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -371,5 +371,36 @@ describe AutocompleteController do
expect(json_response[3]).to match('name' => 'thumbsdown')
end
end
+
+ context 'Get merge_request_target_branches' do
+ let(:user2) { create(:user) }
+ let!(:merge_request1) { create(:merge_request, source_project: project, target_branch: 'feature') }
+
+ context 'unauthorized user' do
+ it 'returns empty json' do
+ get :merge_request_target_branches
+
+ expect(json_response).to be_empty
+ end
+ end
+
+ context 'sign in as user without any accesible merge requests' do
+ it 'returns empty json' do
+ sign_in(user2)
+ get :merge_request_target_branches
+
+ expect(json_response).to be_empty
+ end
+ end
+
+ context 'sign in as user with a accesible merge request' do
+ it 'returns json' do
+ sign_in(user)
+ get :merge_request_target_branches
+
+ expect(json_response).to contain_exactly({ 'title' => 'feature' })
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
index 40673d10b91..15eb0a442a6 100644
--- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
@@ -66,4 +66,77 @@ describe Groups::Settings::CiCdController do
end
end
end
+
+ describe 'PATCH #update_auto_devops' do
+ let(:auto_devops_param) { '1' }
+
+ subject do
+ patch :update_auto_devops, params: {
+ group_id: group,
+ group: { auto_devops_enabled: auto_devops_param }
+ }
+ end
+
+ context 'when user does not have enough permission' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it { is_expected.to have_gitlab_http_status(404) }
+ end
+
+ context 'when user has enough privileges' do
+ before do
+ group.add_owner(user)
+ end
+
+ it { is_expected.to redirect_to(group_settings_ci_cd_path) }
+
+ context 'when service execution went wrong' do
+ before do
+ allow_any_instance_of(Groups::AutoDevopsService).to receive(:execute).and_return(false)
+ allow_any_instance_of(Group).to receive_message_chain(:errors, :full_messages)
+ .and_return(['Error 1'])
+
+ subject
+ end
+
+ it 'returns a flash alert' do
+ expect(response).to set_flash[:alert]
+ .to eq("There was a problem updating Auto DevOps pipeline: [\"Error 1\"].")
+ end
+ end
+
+ context 'when service execution was successful' do
+ it 'returns a flash notice' do
+ subject
+
+ expect(response).to set_flash[:notice]
+ .to eq('Auto DevOps pipeline was updated for the group')
+ end
+ end
+
+ context 'when changing auto devops value' do
+ before do
+ subject
+
+ group.reload
+ end
+
+ context 'when explicitly enabling auto devops' do
+ it 'should update group attribute' do
+ expect(group.auto_devops_enabled).to eq(true)
+ end
+ end
+
+ context 'when explicitly disabling auto devops' do
+ let(:auto_devops_param) { '0' }
+
+ it 'should update group attribute' do
+ expect(group.auto_devops_enabled).to eq(false)
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 21e5122c06b..b2e6df6060a 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -32,21 +32,46 @@ describe GroupsController do
end
end
+ shared_examples 'details view' do
+ it { is_expected.to render_template('groups/show') }
+
+ context 'as atom' do
+ let!(:event) { create(:event, project: project) }
+ let(:format) { :atom }
+
+ it { is_expected.to render_template('groups/show') }
+
+ it 'assigns events for all the projects in the group' do
+ subject
+ expect(assigns(:events)).to contain_exactly(event)
+ end
+ end
+ end
+
describe 'GET #show' do
before do
sign_in(user)
project
end
- context 'as atom' do
- it 'assigns events for all the projects in the group' do
- create(:event, project: project)
+ let(:format) { :html }
- get :show, params: { id: group.to_param }, format: :atom
+ subject { get :show, params: { id: group.to_param }, format: format }
- expect(assigns(:events)).not_to be_empty
- end
+ it_behaves_like 'details view'
+ end
+
+ describe 'GET #details' do
+ before do
+ sign_in(user)
+ project
end
+
+ let(:format) { :html }
+
+ subject { get :details, params: { id: group.to_param }, format: format }
+
+ it_behaves_like 'details view'
end
describe 'GET edit' do
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 8a44ce52849..ee5d27355f1 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -82,6 +82,12 @@ FactoryBot.define do
end
end
+ trait :with_job do
+ after(:build) do |pipeline, evaluator|
+ pipeline.builds << build(:ci_build, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
trait :auto_devops_source do
config_source { Ci::Pipeline.config_sources[:auto_devops_source] }
end
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 3b354c0d96b..dcef8571f41 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -36,5 +36,13 @@ FactoryBot.define do
trait :nested do
parent factory: :group
end
+
+ trait :auto_devops_enabled do
+ auto_devops_enabled true
+ end
+
+ trait :auto_devops_disabled do
+ auto_devops_enabled false
+ end
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 18f724770b5..a73f330a7a9 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -101,12 +101,33 @@ FactoryBot.define do
end
end
- trait :with_merge_request_pipeline do
+ trait :with_detached_merge_request_pipeline do
after(:build) do |merge_request|
merge_request.merge_request_pipelines << build(:ci_pipeline,
source: :merge_request_event,
merge_request: merge_request,
- project: merge_request.source_project)
+ project: merge_request.source_project,
+ ref: merge_request.ref_path,
+ sha: merge_request.source_branch_sha)
+ end
+ end
+
+ trait :with_merge_request_pipeline do
+ transient do
+ merge_sha { 'test-merge-sha' }
+ source_sha { source_branch_sha }
+ target_sha { target_branch_sha }
+ end
+
+ after(:build) do |merge_request, evaluator|
+ merge_request.merge_request_pipelines << create(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request,
+ project: merge_request.source_project,
+ ref: merge_request.merge_ref_path,
+ sha: evaluator.merge_sha,
+ source_sha: evaluator.source_sha,
+ target_sha: evaluator.target_sha)
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 30d3b22d868..ab185ab3972 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -271,6 +271,10 @@ FactoryBot.define do
trait :auto_devops do
association :auto_devops, factory: :project_auto_devops
end
+
+ trait :auto_devops_disabled do
+ association :auto_devops, factory: [:project_auto_devops, :disabled]
+ end
end
# Project with empty repository
diff --git a/spec/features/groups/settings/ci_cd_spec.rb b/spec/features/groups/settings/ci_cd_spec.rb
index d422fd18346..0f793dbab6e 100644
--- a/spec/features/groups/settings/ci_cd_spec.rb
+++ b/spec/features/groups/settings/ci_cd_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
describe 'Group CI/CD settings' do
include WaitForRequests
- let(:user) {create(:user)}
- let(:group) {create(:group)}
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
before do
group.add_owner(user)
@@ -36,4 +36,45 @@ describe 'Group CI/CD settings' do
end
end
end
+
+ describe 'Auto DevOps form' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ context 'as owner first visiting group settings' do
+ it 'should see instance enabled badge' do
+ visit group_settings_ci_cd_path(group)
+
+ page.within '#auto-devops-settings' do
+ expect(page).to have_content('instance enabled')
+ end
+ end
+ end
+
+ context 'when Auto DevOps group has been enabled' do
+ it 'should see group enabled badge' do
+ group.update!(auto_devops_enabled: true)
+
+ visit group_settings_ci_cd_path(group)
+
+ page.within '#auto-devops-settings' do
+ expect(page).to have_content('group enabled')
+ end
+ end
+ end
+
+ context 'when Auto DevOps group has been disabled' do
+ it 'should not see a badge' do
+ group.update!(auto_devops_enabled: false)
+
+ visit group_settings_ci_cd_path(group)
+
+ page.within '#auto-devops-settings' do
+ expect(page).not_to have_content('instance enabled')
+ expect(page).not_to have_content('group enabled')
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb
index 0434db04113..74342b16cb2 100644
--- a/spec/features/merge_request/user_views_diffs_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_spec.rb
@@ -34,6 +34,16 @@ describe 'User views diffs', :js do
expect(page).not_to have_selector('.mr-loading-status .loading', visible: true)
end
+ it 'expands all diffs' do
+ first('#a5cc2925ca8258af241be7e5b0381edf30266302 .js-file-title').click
+
+ expect(page).to have_button('Expand all')
+
+ click_button 'Expand all'
+
+ expect(page).not_to have_button('Expand all')
+ end
+
context 'when in the inline view' do
include_examples 'unfold diffs'
end
diff --git a/spec/features/merge_requests/user_filters_by_target_branch_spec.rb b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb
new file mode 100644
index 00000000000..ffbdacc68f6
--- /dev/null
+++ b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb
@@ -0,0 +1,45 @@
+require 'rails_helper'
+
+describe 'Merge Requests > User filters by target branch', :js do
+ include FilteredSearchHelpers
+
+ let!(:project) { create(:project, :public, :repository) }
+ let!(:user) { project.creator }
+ let!(:mr1) { create(:merge_request, source_project: project, target_project: project, source_branch: 'feature', target_branch: 'master') }
+ let!(:mr2) { create(:merge_request, source_project: project, target_project: project, source_branch: 'feature', target_branch: 'merged-target') }
+
+ before do
+ sign_in(user)
+ visit project_merge_requests_path(project)
+ end
+
+ context 'filtering by target-branch:master' do
+ it 'applies the filter' do
+ input_filtered_search('target-branch:master')
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ expect(page).to have_content mr1.title
+ expect(page).not_to have_content mr2.title
+ end
+ end
+
+ context 'filtering by target-branch:merged-target' do
+ it 'applies the filter' do
+ input_filtered_search('target-branch:merged-target')
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ expect(page).not_to have_content mr1.title
+ expect(page).to have_content mr2.title
+ end
+ end
+
+ context 'filtering by target-branch:feature' do
+ it 'applies the filter' do
+ input_filtered_search('target-branch:feature')
+
+ expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
+ expect(page).not_to have_content mr1.title
+ expect(page).not_to have_content mr2.title
+ end
+ end
+end
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
index 4981bf794d9..aa1c3902f0f 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -227,7 +227,7 @@ describe 'Clusters Applications', :js do
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
expect(page).to have_css('.js-cluster-application-install-button[disabled]')
expect(page).to have_selector('.js-no-endpoint-message')
- expect(page.find('.js-endpoint').value).to eq('?')
+ expect(page).to have_selector('.js-ingress-ip-loading-icon')
# We receive the external IP address and display
Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index 4c85abe9971..bf0c0de89b2 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -110,6 +110,37 @@ describe "Projects > Settings > Pipelines settings" do
expect(page).not_to have_content('instance enabled')
end
end
+
+ context 'when auto devops is turned on group level' do
+ before do
+ project.update!(namespace: create(:group, :auto_devops_enabled))
+ end
+
+ it 'renders group enabled badge' do
+ visit project_settings_ci_cd_path(project)
+
+ page.within '#autodevops-settings' do
+ expect(page).to have_content('group enabled')
+ expect(find_field('project_auto_devops_attributes_enabled')).to be_checked
+ end
+ end
+ end
+
+ context 'when auto devops is turned on group parent level', :nested_groups do
+ before do
+ group = create(:group, parent: create(:group, :auto_devops_enabled))
+ project.update!(namespace: group)
+ end
+
+ it 'renders group enabled badge' do
+ visit project_settings_ci_cd_path(project)
+
+ page.within '#autodevops-settings' do
+ expect(page).to have_content('group enabled')
+ expect(find_field('project_auto_devops_attributes_enabled')).to be_checked
+ end
+ end
+ end
end
end
diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb
index ffa80235083..0d59ef4a727 100644
--- a/spec/features/projects/show/user_sees_git_instructions_spec.rb
+++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb
@@ -13,7 +13,7 @@ describe 'Projects > Show > User sees Git instructions' do
it 'shows Git command line instructions' do
click_link 'Create empty repository'
- page.within '.empty_wrapper' do
+ page.within '.empty-wrapper' do
expect(page).to have_content('Command line instructions')
end
end
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index dcca1d388c7..58bd20d7551 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -20,18 +20,18 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
end
- it 'no Auto DevOps button if can not manage pipelines' do
- page.within('.project-buttons') do
- expect(page).not_to have_link('Enable Auto DevOps')
- expect(page).not_to have_link('Auto DevOps enabled')
- end
- end
-
- it '"Auto DevOps enabled" button not linked' do
+ it 'Project buttons are not visible' do
visit project_path(project)
page.within('.project-buttons') do
- expect(page).to have_text('Auto DevOps enabled')
+ expect(page).not_to have_link('New file')
+ expect(page).not_to have_link('Add README')
+ expect(page).not_to have_link('Add CHANGELOG')
+ expect(page).not_to have_link('Add CONTRIBUTING')
+ expect(page).not_to have_link('Enable Auto DevOps')
+ expect(page).not_to have_link('Auto DevOps enabled')
+ expect(page).not_to have_link('Add Kubernetes cluster')
+ expect(page).not_to have_link('Kubernetes configured')
end
end
end
@@ -61,46 +61,6 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
expect(page).to have_link('Add license', href: presenter.add_license_path)
end
end
-
- describe 'Auto DevOps button' do
- context 'when Auto DevOps is enabled' do
- it '"Auto DevOps enabled" anchor linked to settings page' do
- visit project_path(project)
-
- page.within('.project-buttons') do
- expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
- end
- end
- end
-
- context 'when Auto DevOps is not enabled' do
- let(:project) { create(:project, :public, :empty_repo, auto_devops_attributes: { enabled: false }) }
-
- it '"Enable Auto DevOps" button linked to settings page' do
- page.within('.project-buttons') do
- expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
- end
- end
- end
- end
-
- describe 'Kubernetes cluster button' do
- it '"Add Kubernetes cluster" button linked to clusters page' do
- page.within('.project-buttons') do
- expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project))
- end
- end
-
- it '"Kubernetes cluster" anchor linked to cluster page' do
- cluster = create(:cluster, :provided_by_gcp, projects: [project])
-
- visit project_path(project)
-
- page.within('.project-buttons') do
- expect(page).to have_link('Kubernetes configured', href: project_cluster_path(project, cluster))
- end
- end
- end
end
end
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index 3238e07fe15..a776169a8e5 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -27,7 +27,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
- it { is_expected.to be_denied_for(project_guest) }
+ it { is_expected.to be_allowed_for(project_guest) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
@@ -42,7 +42,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
- it { is_expected.to be_denied_for(project_guest) }
+ it { is_expected.to be_allowed_for(project_guest) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
@@ -58,7 +58,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
- it { is_expected.to be_denied_for(project_guest) }
+ it { is_expected.to be_allowed_for(project_guest) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
@@ -73,7 +73,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
- it { is_expected.to be_denied_for(project_guest) }
+ it { is_expected.to be_allowed_for(project_guest) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
@@ -96,12 +96,9 @@ describe 'Private Group access' do
describe 'GET /groups/:path for shared projects' do
let(:project) { create(:project, :public) }
+
before do
- Projects::GroupLinks::CreateService.new(
- project,
- create(:user),
- link_group_access: ProjectGroupLink::DEVELOPER
- ).execute(group)
+ create(:project_group_link, project: project, group: group)
end
subject { group_path(group) }
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index 9abc52aa664..3f060ba0553 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -209,6 +209,12 @@ describe LabelsFinder do
expect(finder.execute).to eq [project_label_1]
end
+
+ it 'returns labels matching a single character' do
+ finder = described_class.new(user, search: '(')
+
+ expect(finder.execute).to eq [group_label_1]
+ end
end
context 'filter by subscription' do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 503b88fcbad..f1178b07eec 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -36,7 +36,7 @@ describe MergeRequestsFinder do
let(:project5) { create_project_without_n_plus_1(group: subgroup) }
let(:project6) { create_project_without_n_plus_1(group: subgroup) }
- let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
+ let!(:merge_request1) { create(:merge_request, author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') }
let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') }
let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') }
diff --git a/spec/fixtures/api/schemas/board.json b/spec/fixtures/api/schemas/board.json
index 03aca4a3cc0..7c146647948 100644
--- a/spec/fixtures/api/schemas/board.json
+++ b/spec/fixtures/api/schemas/board.json
@@ -6,6 +6,5 @@
"properties" : {
"id": { "type": "integer" },
"name": { "type": "string" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
index 00abe73ec8a..162fb9c8108 100644
--- a/spec/fixtures/api/schemas/entities/issue.json
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -38,6 +38,5 @@
"items": { "$ref": "label.json" }
},
"assignees": { "type": ["array", "null"] }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/issue_boards.json b/spec/fixtures/api/schemas/entities/issue_boards.json
index 0ac1d9468c8..742f7be5485 100644
--- a/spec/fixtures/api/schemas/entities/issue_boards.json
+++ b/spec/fixtures/api/schemas/entities/issue_boards.json
@@ -10,6 +10,5 @@
"items": { "$ref": "issue_board.json" }
},
"size": { "type": "integer" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json
index 67c209f3fc3..6b1cd60c25d 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -52,6 +52,7 @@
"mergeable_discussions_state": { "type": "boolean" },
"conflicts_can_be_resolved_in_ui": { "type": "boolean" },
"branch_missing": { "type": "boolean" },
+ "commits_count": { "type": ["integer", "null"] },
"has_conflicts": { "type": "boolean" },
"can_be_merged": { "type": "boolean" },
"mergeable": { "type": "boolean" },
@@ -125,6 +126,5 @@
"can_receive_suggestion": { "type": "boolean" },
"source_branch_protected": { "type": "boolean" },
"conflicts_docs_path": { "type": ["string", "null"] }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index a83ec55cede..77de9ae4f9f 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -28,7 +28,7 @@
"items": { "$ref": "entities/label.json" }
},
"assignee": {
- "id": { "type": "integet" },
+ "id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
@@ -52,6 +52,5 @@
}
},
"subscribed": { "type": ["boolean", "null"] }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json
index 70771b21c96..fbcd9eea389 100644
--- a/spec/fixtures/api/schemas/issues.json
+++ b/spec/fixtures/api/schemas/issues.json
@@ -10,6 +10,5 @@
"items": { "$ref": "issue.json" }
},
"size": { "type": "integer" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
new file mode 100644
index 00000000000..cd50be00418
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
@@ -0,0 +1,124 @@
+{
+ "type": "object",
+ "properties" : {
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "merged_by": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "merged_at": { "type": ["date", "null"] },
+ "closed_by": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "closed_at": { "type": ["date", "null"] },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "target_branch": { "type": "string" },
+ "source_branch": { "type": "string" },
+ "upvotes": { "type": "integer" },
+ "downvotes": { "type": "integer" },
+ "author": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "assignee": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "source_project_id": { "type": "integer" },
+ "target_project_id": { "type": "integer" },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "work_in_progress": { "type": "boolean" },
+ "milestone": {
+ "type": ["object", "null"],
+ "properties": {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": ["integer", "null"] },
+ "group_id": { "type": ["integer", "null"] },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "due_date": { "type": "date" },
+ "start_date": { "type": "date" }
+ },
+ "additionalProperties": false
+ },
+ "merge_when_pipeline_succeeds": { "type": "boolean" },
+ "merge_status": { "type": "string" },
+ "sha": { "type": "string" },
+ "merge_commit_sha": { "type": ["string", "null"] },
+ "user_notes_count": { "type": "integer" },
+ "changes_count": { "type": "string" },
+ "should_remove_source_branch": { "type": ["boolean", "null"] },
+ "force_remove_source_branch": { "type": ["boolean", "null"] },
+ "discussion_locked": { "type": ["boolean", "null"] },
+ "web_url": { "type": "uri" },
+ "squash": { "type": "boolean" },
+ "time_stats": {
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["string", "null"] },
+ "human_total_time_spent": { "type": ["string", "null"] }
+ },
+ "allow_collaboration": { "type": ["boolean", "null"] },
+ "allow_maintainer_to_push": { "type": ["boolean", "null"] }
+ },
+ "required": [
+ "id", "iid", "project_id", "title", "description",
+ "state", "created_at", "updated_at", "target_branch",
+ "source_branch", "upvotes", "downvotes", "author",
+ "assignee", "source_project_id", "target_project_id",
+ "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
+ "merge_status", "sha", "merge_commit_sha", "user_notes_count",
+ "should_remove_source_branch", "force_remove_source_branch",
+ "web_url", "squash"
+ ]
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
index 6df27bf32b9..b35c83950e8 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -1,126 +1,6 @@
{
"type": "array",
"items": {
- "type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": "integer" },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "merged_by": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "merged_at": { "type": ["date", "null"] },
- "closed_by": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "closed_at": { "type": ["date", "null"] },
- "created_at": { "type": "date" },
- "updated_at": { "type": "date" },
- "target_branch": { "type": "string" },
- "source_branch": { "type": "string" },
- "upvotes": { "type": "integer" },
- "downvotes": { "type": "integer" },
- "author": {
- "type": "object",
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "assignee": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "source_project_id": { "type": "integer" },
- "target_project_id": { "type": "integer" },
- "labels": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "work_in_progress": { "type": "boolean" },
- "milestone": {
- "type": ["object", "null"],
- "properties": {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": ["integer", "null"] },
- "group_id": { "type": ["integer", "null"] },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "date" },
- "updated_at": { "type": "date" },
- "due_date": { "type": "date" },
- "start_date": { "type": "date" }
- },
- "additionalProperties": false
- },
- "merge_when_pipeline_succeeds": { "type": "boolean" },
- "merge_status": { "type": "string" },
- "sha": { "type": "string" },
- "merge_commit_sha": { "type": ["string", "null"] },
- "user_notes_count": { "type": "integer" },
- "changes_count": { "type": "string" },
- "should_remove_source_branch": { "type": ["boolean", "null"] },
- "force_remove_source_branch": { "type": ["boolean", "null"] },
- "discussion_locked": { "type": ["boolean", "null"] },
- "web_url": { "type": "uri" },
- "squash": { "type": "boolean" },
- "time_stats": {
- "time_estimate": { "type": "integer" },
- "total_time_spent": { "type": "integer" },
- "human_time_estimate": { "type": ["string", "null"] },
- "human_total_time_spent": { "type": ["string", "null"] }
- },
- "allow_collaboration": { "type": ["boolean", "null"] },
- "allow_maintainer_to_push": { "type": ["boolean", "null"] }
- },
- "required": [
- "id", "iid", "project_id", "title", "description",
- "state", "created_at", "updated_at", "target_branch",
- "source_branch", "upvotes", "downvotes", "author",
- "assignee", "source_project_id", "target_project_id",
- "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
- "merge_status", "sha", "merge_commit_sha", "user_notes_count",
- "should_remove_source_branch", "force_remove_source_branch",
- "web_url", "squash"
- ],
- "additionalProperties": false
+ "$ref": "./merge_request.json"
}
}
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index c7008c780d6..27c679c749b 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -3,8 +3,8 @@
import $ from 'jquery';
import GfmAutoComplete from '~/gfm_auto_complete';
-import 'vendor/jquery.caret';
-import 'vendor/jquery.atwho';
+import 'jquery.caret';
+import 'at.js';
describe('GfmAutoComplete', () => {
const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
diff --git a/spec/frontend/helpers/timeout.js b/spec/frontend/helpers/timeout.js
new file mode 100644
index 00000000000..318593a48a4
--- /dev/null
+++ b/spec/frontend/helpers/timeout.js
@@ -0,0 +1,24 @@
+let testTimeoutInMs;
+
+export const setTestTimeout = newTimeoutInMs => {
+ testTimeoutInMs = newTimeoutInMs;
+ jest.setTimeout(newTimeoutInMs);
+};
+
+export const initializeTestTimeout = defaultTimeoutInMs => {
+ setTestTimeout(defaultTimeoutInMs);
+
+ let testStartTime;
+
+ // https://github.com/facebook/jest/issues/6947
+ beforeEach(() => {
+ testStartTime = Date.now();
+ });
+
+ afterEach(() => {
+ const elapsedTimeInMs = Date.now() - testStartTime;
+ if (elapsedTimeInMs > testTimeoutInMs) {
+ throw new Error(`Test took too long (${elapsedTimeInMs}ms > ${testTimeoutInMs}ms)!`);
+ }
+ });
+};
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index d892889b98d..8c36d8ff49f 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -1,23 +1,9 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import axios from '~/lib/utils/axios_utils';
+import { initializeTestTimeout } from './helpers/timeout';
-const testTimeoutInMs = 300;
-jest.setTimeout(testTimeoutInMs);
-
-let testStartTime;
-
-// https://github.com/facebook/jest/issues/6947
-beforeEach(() => {
- testStartTime = Date.now();
-});
-
-afterEach(() => {
- const elapsedTimeInMs = Date.now() - testStartTime;
- if (elapsedTimeInMs > testTimeoutInMs) {
- throw new Error(`Test took too long (${elapsedTimeInMs}ms > ${testTimeoutInMs}ms)!`);
- }
-});
+initializeTestTimeout(300);
// fail tests for unmocked requests
beforeEach(done => {
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
index 223e562238d..d2540696b17 100644
--- a/spec/helpers/auto_devops_helper_spec.rb
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -29,11 +29,11 @@ describe AutoDevopsHelper do
end
context 'when the banner is disabled by feature flag' do
- it 'allows the feature flag to disable' do
+ before do
Feature.get(:auto_devops_banner_disabled).enable
-
- expect(subject).to be(false)
end
+
+ it { is_expected.to be_falsy }
end
context 'when dismissed' do
@@ -90,4 +90,136 @@ describe AutoDevopsHelper do
it { is_expected.to eq(false) }
end
end
+
+ describe '#badge_for_auto_devops_scope' do
+ subject { helper.badge_for_auto_devops_scope(receiver) }
+
+ context 'when receiver is a group' do
+ context 'when explicitly enabled' do
+ let(:receiver) { create(:group, :auto_devops_enabled) }
+
+ it { is_expected.to eq('group enabled') }
+ end
+
+ context 'when explicitly disabled' do
+ let(:receiver) { create(:group, :auto_devops_disabled) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when auto devops is implicitly enabled' do
+ let(:receiver) { create(:group) }
+
+ context 'by instance' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it { is_expected.to eq('instance enabled') }
+ end
+
+ context 'with groups', :nested_groups do
+ before do
+ receiver.update(parent: parent)
+ end
+
+ context 'when auto devops is enabled on parent' do
+ let(:parent) { create(:group, :auto_devops_enabled) }
+
+ it { is_expected.to eq('group enabled') }
+ end
+
+ context 'when auto devops is enabled on parent group' do
+ let(:root_parent) { create(:group, :auto_devops_enabled) }
+ let(:parent) { create(:group, parent: root_parent) }
+
+ it { is_expected.to eq('group enabled') }
+ end
+
+ context 'when auto devops disabled set on parent group' do
+ let(:root_parent) { create(:group, :auto_devops_disabled) }
+ let(:parent) { create(:group, parent: root_parent) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
+
+ context 'when receiver is a project' do
+ context 'when auto devops is enabled at project level' do
+ let(:receiver) { create(:project, :auto_devops) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when auto devops is disabled at project level' do
+ let(:receiver) { create(:project, :auto_devops_disabled) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when auto devops is implicitly enabled' do
+ let(:receiver) { create(:project) }
+
+ context 'by instance' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it { is_expected.to eq('instance enabled') }
+ end
+
+ context 'with groups', :nested_groups do
+ let(:receiver) { create(:project, :repository, namespace: group) }
+
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ context 'when auto devops is enabled on group level' do
+ let(:group) { create(:group, :auto_devops_enabled) }
+
+ it { is_expected.to eq('group enabled') }
+ end
+
+ context 'when auto devops is enabled on root group' do
+ let(:root_parent) { create(:group, :auto_devops_enabled) }
+ let(:group) { create(:group, parent: root_parent) }
+
+ it { is_expected.to eq('group enabled') }
+ end
+ end
+ end
+
+ context 'when auto devops is implicitly disabled' do
+ let(:receiver) { create(:project) }
+
+ context 'by instance' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with groups', :nested_groups do
+ let(:receiver) { create(:project, :repository, namespace: group) }
+
+ context 'when auto devops is disabled on group level' do
+ let(:group) { create(:group, :auto_devops_disabled) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when root group is enabled and parent disabled' do
+ let(:root_parent) { create(:group, :auto_devops_enabled) }
+ let(:group) { create(:group, :auto_devops_disabled, parent: root_parent) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index e2466bf326c..790e4b9602c 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -141,7 +141,7 @@ describe('Applications', () => {
});
describe('without ip address', () => {
- it('renders an input text with a question mark and an alert text', () => {
+ it('renders an input text with a loading icon and an alert text', () => {
vm = mountComponent(Applications, {
applications: {
...APPLICATIONS_MOCK_STATE,
@@ -152,8 +152,7 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-endpoint').value).toEqual('?');
-
+ expect(vm.$el.querySelector('.js-ingress-ip-loading-icon')).not.toBe(null);
expect(vm.$el.querySelector('.js-no-endpoint-message')).not.toBe(null);
});
});
@@ -330,7 +329,7 @@ describe('Applications', () => {
});
describe('without ip address', () => {
- it('renders an input text with a question mark and an alert text', () => {
+ it('renders an input text with a loading icon and an alert text', () => {
vm = mountComponent(Applications, {
applications: {
...APPLICATIONS_MOCK_STATE,
@@ -342,8 +341,7 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-knative-endpoint').value).toEqual('?');
-
+ expect(vm.$el.querySelector('.js-knative-ip-loading-icon')).not.toBe(null);
expect(vm.$el.querySelector('.js-no-knative-endpoint-message')).not.toBe(null);
});
});
diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
index ba04c8c4a4c..d9b298e84da 100644
--- a/spec/javascripts/diffs/components/diff_file_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -109,6 +109,31 @@ describe('DiffFile', () => {
done();
});
});
+
+ it('should update store state', done => {
+ spyOn(vm.$store, 'dispatch');
+
+ vm.isCollapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/setFileCollapsed', {
+ filePath: vm.file.file_path,
+ collapsed: true,
+ });
+
+ done();
+ });
+ });
+
+ it('updates local state when changing file state', done => {
+ vm.file.viewer.collapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.isCollapsed).toBe(true);
+
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 070bfb2ccd0..6c637097893 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -35,6 +35,7 @@ import actions, {
receiveFullDiffError,
fetchFullDiff,
toggleFullDiff,
+ setFileCollapsed,
} from '~/diffs/store/actions';
import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types';
@@ -977,4 +978,17 @@ describe('DiffsStoreActions', () => {
);
});
});
+
+ describe('setFileCollapsed', () => {
+ it('commits SET_FILE_COLLAPSED', done => {
+ testAction(
+ setFileCollapsed,
+ { filePath: 'test', collapsed: true },
+ null,
+ [{ type: types.SET_FILE_COLLAPSED, payload: { filePath: 'test', collapsed: true } }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index 270e7d75666..5556a994a14 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -58,13 +58,15 @@ describe('DiffsStoreMutations', () => {
describe('EXPAND_ALL_FILES', () => {
it('should change the collapsed prop from diffFiles', () => {
const diffFile = {
- collapsed: true,
+ viewer: {
+ collapsed: true,
+ },
};
const state = { expandAllFiles: true, diffFiles: [diffFile] };
mutations[types.EXPAND_ALL_FILES](state);
- expect(state.diffFiles[0].collapsed).toEqual(false);
+ expect(state.diffFiles[0].viewer.collapsed).toEqual(false);
});
});
@@ -742,4 +744,16 @@ describe('DiffsStoreMutations', () => {
expect(state.diffFiles[0].isShowingFullFile).toBe(true);
});
});
+
+ describe('SET_FILE_COLLAPSED', () => {
+ it('sets collapsed', () => {
+ const state = {
+ diffFiles: [{ file_path: 'test', viewer: { collapsed: false } }],
+ };
+
+ mutations[types.SET_FILE_COLLAPSED](state, { filePath: 'test', collapsed: true });
+
+ expect(state.diffFiles[0].viewer.collapsed).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js
index a89e50045da..8b877994515 100644
--- a/spec/javascripts/environments/environment_item_spec.js
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -20,23 +20,23 @@ describe('Environment item', () => {
size: 3,
isFolder: true,
environment_path: 'url',
+ log_path: 'url',
};
component = new EnvironmentItem({
propsData: {
model: mockItem,
canReadEnvironment: true,
- service: {},
},
}).$mount();
});
- it('Should render folder icon and name', () => {
+ it('should render folder icon and name', () => {
expect(component.$el.querySelector('.folder-name').textContent).toContain(mockItem.name);
expect(component.$el.querySelector('.folder-icon')).toBeDefined();
});
- it('Should render the number of children in a badge', () => {
+ it('should render the number of children in a badge', () => {
expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(
mockItem.size,
);
@@ -109,6 +109,7 @@ describe('Environment item', () => {
},
has_stop_action: true,
environment_path: 'root/ci-folders/environments/31',
+ log_path: 'root/ci-folders/environments/31/logs',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
};
@@ -117,7 +118,6 @@ describe('Environment item', () => {
propsData: {
model: environment,
canReadEnvironment: true,
- service: {},
},
}).$mount();
});
@@ -157,13 +157,13 @@ describe('Environment item', () => {
});
describe('With build url', () => {
- it('Should link to build url provided', () => {
+ it('should link to build url provided', () => {
expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual(
environment.last_deployment.deployable.build_path,
);
});
- it('Should render deployable name and id', () => {
+ it('should render deployable name and id', () => {
expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual(
environment.last_deployment.deployable.build_path,
);
@@ -178,7 +178,7 @@ describe('Environment item', () => {
});
describe('With manual actions', () => {
- it('Should render actions component', () => {
+ it('should render actions component', () => {
expect(component.$el.querySelector('.js-manual-actions-container')).toBeDefined();
});
});
@@ -190,13 +190,13 @@ describe('Environment item', () => {
});
describe('With stop action', () => {
- it('Should render stop action component', () => {
+ it('should render stop action component', () => {
expect(component.$el.querySelector('.js-stop-component-container')).toBeDefined();
});
});
describe('With retry action', () => {
- it('Should render rollback component', () => {
+ it('should render rollback component', () => {
expect(component.$el.querySelector('.js-rollback-component-container')).toBeDefined();
});
});
diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js
index ecd28594873..a3f34232a85 100644
--- a/spec/javascripts/environments/environment_table_spec.js
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -6,6 +6,14 @@ describe('Environment table', () => {
let Component;
let vm;
+ const eeOnlyProps = {
+ canaryDeploymentFeatureId: 'canary_deployment',
+ showCanaryDeploymentCallout: true,
+ userCalloutsPath: '/callouts',
+ lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
+ helpCanaryDeploymentsPath: 'help/canary-deployments',
+ };
+
beforeEach(() => {
Component = Vue.extend(environmentTableComp);
});
@@ -27,6 +35,7 @@ describe('Environment table', () => {
vm = mountComponent(Component, {
environments: [mockItem],
canReadEnvironment: true,
+ ...eeOnlyProps,
});
expect(vm.$el.getAttribute('class')).toContain('ci-table');
@@ -67,6 +76,7 @@ describe('Environment table', () => {
vm = mountComponent(Component, {
environments: mockItems,
canReadEnvironment: true,
+ ...eeOnlyProps,
});
const [old, newer, older, noDeploy] = mockItems;
@@ -130,6 +140,7 @@ describe('Environment table', () => {
vm = mountComponent(Component, {
environments: mockItems,
canReadEnvironment: true,
+ ...eeOnlyProps,
});
const [prod, review, staging] = mockItems;
@@ -166,6 +177,7 @@ describe('Environment table', () => {
vm = mountComponent(Component, {
environments: mockItems,
canReadEnvironment: true,
+ ...eeOnlyProps,
});
const [old, newer, older] = mockItems;
@@ -192,6 +204,7 @@ describe('Environment table', () => {
vm = mountComponent(Component, {
environments: mockItems,
canReadEnvironment: true,
+ ...eeOnlyProps,
});
const [old, newer, older] = mockItems;
@@ -240,6 +253,7 @@ describe('Environment table', () => {
vm = mountComponent(Component, {
environments: mockItems,
canReadEnvironment: true,
+ ...eeOnlyProps,
});
expect(vm.sortedEnvironments.map(env => env.name)).toEqual([
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
index b6a244f7cd3..0dcd8868aba 100644
--- a/spec/javascripts/environments/environments_app_spec.js
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -13,6 +13,11 @@ describe('Environment', () => {
cssContainerClass: 'container',
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
+ canaryDeploymentFeatureId: 'canary_deployment',
+ showCanaryDeploymentCallout: true,
+ userCalloutsPath: '/callouts',
+ lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
+ helpCanaryDeploymentsPath: 'help/canary-deployments',
};
let EnvironmentsComponent;
diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js
index c3d16f10d72..8abdbcbbe54 100644
--- a/spec/javascripts/environments/environments_store_spec.js
+++ b/spec/javascripts/environments/environments_store_spec.js
@@ -34,54 +34,46 @@ describe('Store', () => {
expect(store.state.stoppedCounter).toEqual(2);
});
- describe('store environments', () => {
- it('should store environments', () => {
- store.storeEnvironments(serverData);
-
- expect(store.state.environments.length).toEqual(serverData.length);
- });
-
- it('should add folder keys when environment is a folder', () => {
- const environment = {
- name: 'bar',
- size: 3,
- id: 2,
- };
+ it('should add folder keys when environment is a folder', () => {
+ const environment = {
+ name: 'bar',
+ size: 3,
+ id: 2,
+ };
- store.storeEnvironments([environment]);
+ store.storeEnvironments([environment]);
- expect(store.state.environments[0].isFolder).toEqual(true);
- expect(store.state.environments[0].folderName).toEqual('bar');
- });
-
- it('should extract content of `latest` key when provided', () => {
- const environment = {
- name: 'bar',
- size: 3,
- id: 2,
- latest: {
- last_deployment: {},
- isStoppable: true,
- },
- };
-
- store.storeEnvironments([environment]);
+ expect(store.state.environments[0].isFolder).toEqual(true);
+ expect(store.state.environments[0].folderName).toEqual('bar');
+ });
- expect(store.state.environments[0].last_deployment).toEqual({});
- expect(store.state.environments[0].isStoppable).toEqual(true);
- });
+ it('should extract content of `latest` key when provided', () => {
+ const environment = {
+ name: 'bar',
+ size: 3,
+ id: 2,
+ latest: {
+ last_deployment: {},
+ isStoppable: true,
+ },
+ };
+
+ store.storeEnvironments([environment]);
+
+ expect(store.state.environments[0].last_deployment).toEqual({});
+ expect(store.state.environments[0].isStoppable).toEqual(true);
+ });
- it('should store latest.name when the environment is not a folder', () => {
- store.storeEnvironments(serverData);
+ it('should store latest.name when the environment is not a folder', () => {
+ store.storeEnvironments(serverData);
- expect(store.state.environments[0].name).toEqual(serverData[0].latest.name);
- });
+ expect(store.state.environments[0].name).toEqual(serverData[0].latest.name);
+ });
- it('should store root level name when environment is a folder', () => {
- store.storeEnvironments(serverData);
+ it('should store root level name when environment is a folder', () => {
+ store.storeEnvironments(serverData);
- expect(store.state.environments[1].folderName).toEqual(serverData[1].name);
- });
+ expect(store.state.environments[1].folderName).toEqual(serverData[1].name);
});
describe('toggleFolder', () => {
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index 69ddd26eef1..ff15067aeac 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { removeBreakLine, removeWhitespace } from 'spec/helpers/vue_component_helper';
import { environmentsList } from '../mock_data';
describe('Environments Folder View', () => {
@@ -15,6 +16,11 @@ describe('Environments Folder View', () => {
folderName: 'review',
canReadEnvironment: true,
cssContainerClass: 'container',
+ canaryDeploymentFeatureId: 'canary_deployment',
+ showCanaryDeploymentCallout: true,
+ userCalloutsPath: '/callouts',
+ lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
+ helpCanaryDeploymentsPath: 'help/canary-deployments',
};
beforeEach(() => {
@@ -89,9 +95,11 @@ describe('Environments Folder View', () => {
it('should render parent folder name', done => {
setTimeout(() => {
- expect(component.$el.querySelector('.js-folder-name').textContent.trim()).toContain(
- 'Environments / review',
- );
+ expect(
+ removeBreakLine(
+ removeWhitespace(component.$el.querySelector('.js-folder-name').textContent.trim()),
+ ),
+ ).toContain('Environments / review');
done();
}, 0);
});
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index f3dc35552d5..a72ea6ab547 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -293,6 +293,7 @@ describe('Filtered Search Visual Tokens', () => {
subject.addVisualTokenElement('milestone');
const token = tokensContainer.querySelector('.js-visual-token');
+ expect(token.classList.contains('search-token-milestone')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('milestone');
expect(token.querySelector('.value')).toEqual(null);
@@ -302,6 +303,7 @@ describe('Filtered Search Visual Tokens', () => {
subject.addVisualTokenElement('label', 'Frontend');
const token = tokensContainer.querySelector('.js-visual-token');
+ expect(token.classList.contains('search-token-label')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('label');
expect(token.querySelector('.value').innerText).toEqual('Frontend');
@@ -317,10 +319,12 @@ describe('Filtered Search Visual Tokens', () => {
const labelToken = tokens[0];
const assigneeToken = tokens[1];
+ expect(labelToken.classList.contains('search-token-label')).toEqual(true);
expect(labelToken.classList.contains('filtered-search-token')).toEqual(true);
expect(labelToken.querySelector('.name').innerText).toEqual('label');
expect(labelToken.querySelector('.value').innerText).toEqual('Frontend');
+ expect(assigneeToken.classList.contains('search-token-assignee')).toEqual(true);
expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true);
expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee');
expect(assigneeToken.querySelector('.value').innerText).toEqual('@root');
diff --git a/spec/javascripts/fixtures/emojis.rb b/spec/javascripts/fixtures/emojis.rb
index f5fb008c7b3..4dab697e5e2 100644
--- a/spec/javascripts/fixtures/emojis.rb
+++ b/spec/javascripts/fixtures/emojis.rb
@@ -9,6 +9,8 @@ describe 'Emojis (JavaScript fixtures)' do
it 'emojis/emojis.json' do |example|
JavaScriptFixturesHelpers::FIXTURE_PATHS.each do |fixture_path|
+ next unless File.directory?(fixture_path)
+
# Copying the emojis.json from the public folder
fixture_file_name = File.expand_path('emojis/emojis.json', fixture_path)
FileUtils.mkdir_p(File.dirname(fixture_file_name))
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
index 8933dd5def4..fd06bb1f324 100644
--- a/spec/javascripts/helpers/filtered_search_spec_helper.js
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -5,7 +5,7 @@ export default class FilteredSearchSpecHelper {
static createFilterVisualToken(name, value, isSelected = false) {
const li = document.createElement('li');
- li.classList.add('js-visual-token', 'filtered-search-token');
+ li.classList.add('js-visual-token', 'filtered-search-token', `search-token-${name}`);
li.innerHTML = `
<div class="selectable ${isSelected ? 'selected' : ''}" role="button">
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 5eef5682bbd..235a17d13b0 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -69,7 +69,7 @@ window.gl = window.gl || {};
window.gl.TEST_HOST = TEST_HOST;
window.gon = window.gon || {};
window.gon.test_env = true;
-window.gon.ee = false;
+window.gon.ee = process.env.EE;
gon.relative_url_root = '';
let hasUnhandledPromiseRejections = false;
@@ -122,19 +122,26 @@ afterEach(() => {
const axiosDefaultAdapter = getDefaultAdapter();
// render all of our tests
-const testsContext = require.context('.', true, /_spec$/);
-testsContext.keys().forEach(function(path) {
- try {
- testsContext(path);
- } catch (err) {
- console.log(err);
- console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path);
- describe('Test bundle', function() {
- it(`includes '${path}'`, function() {
- expect(err).toBeNull();
+const testContexts = [require.context('spec', true, /_spec$/)];
+
+if (process.env.EE) {
+ testContexts.push(require.context('ee_spec', true, /_spec$/));
+}
+
+testContexts.forEach(context => {
+ context.keys().forEach(path => {
+ try {
+ context(path);
+ } catch (err) {
+ console.log(err);
+ console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path);
+ describe('Test bundle', function() {
+ it(`includes '${path}'`, function() {
+ expect(err).toBeNull();
+ });
});
- });
- }
+ }
+ });
});
describe('test errors', () => {
@@ -204,24 +211,35 @@ if (process.env.BABEL_ENV === 'coverage') {
];
describe('Uncovered files', function() {
- const sourceFiles = require.context('~', true, /\.(js|vue)$/);
+ const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)];
+
+ if (process.env.EE) {
+ sourceFilesContexts.push(require.context('ee', true, /\.(js|vue)$/));
+ }
+
+ const allTestFiles = testContexts.reduce(
+ (accumulator, context) => accumulator.concat(context.keys()),
+ [],
+ );
$.holdReady(true);
- sourceFiles.keys().forEach(function(path) {
- // ignore if there is a matching spec file
- if (testsContext.keys().indexOf(`${path.replace(/\.(js|vue)$/, '')}_spec`) > -1) {
- return;
- }
-
- it(`includes '${path}'`, function() {
- try {
- sourceFiles(path);
- } catch (err) {
- if (troubleMakers.indexOf(path) === -1) {
- expect(err).toBeNull();
- }
+ sourceFilesContexts.forEach(context => {
+ context.keys().forEach(path => {
+ // ignore if there is a matching spec file
+ if (allTestFiles.indexOf(`${path.replace(/\.(js|vue)$/, '')}_spec`) > -1) {
+ return;
}
+
+ it(`includes '${path}'`, function() {
+ try {
+ context(path);
+ } catch (err) {
+ if (troubleMakers.indexOf(path) === -1) {
+ expect(err).toBeNull();
+ }
+ }
+ });
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
index 5cf6408cf34..9ee2f88c78d 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js
@@ -15,6 +15,7 @@ describe('Commits header component', () => {
isSquashEnabled: false,
targetBranch: 'master',
commitsCount: 5,
+ isFastForwardEnabled: false,
...props,
},
});
@@ -31,6 +32,27 @@ describe('Commits header component', () => {
const findTargetBranchMessage = () => wrapper.find('.label-branch');
const findModifyButton = () => wrapper.find('.modify-message-button');
+ describe('when fast-forward is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ isFastForwardEnabled: true,
+ isSquashEnabled: true,
+ });
+ });
+
+ it('has commits count message showing 1 commit', () => {
+ expect(findCommitsCountMessage().text()).toBe('1 commit');
+ });
+
+ it('has button with modify commit message', () => {
+ expect(findModifyButton().text()).toBe('Modify commit message');
+ });
+
+ it('does not have merge commit part of the message', () => {
+ expect(findHeaderWrapper().text()).not.toContain('1 merge commit');
+ });
+ });
+
describe('when collapsed', () => {
it('toggle has aria-label equal to Expand', () => {
createComponent();
@@ -78,6 +100,10 @@ describe('Commits header component', () => {
expect(findTargetBranchMessage().text()).toBe('master');
});
+
+ it('does has merge commit part of the message', () => {
+ expect(findHeaderWrapper().text()).toContain('1 merge commit');
+ });
});
describe('when expanded', () => {
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 08e173b0a10..30659ad16f3 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -18,6 +18,7 @@ const createTestMr = customConfig => {
isPipelinePassing: false,
isMergeAllowed: true,
onlyAllowMergeIfPipelineSucceeds: false,
+ ffOnlyEnabled: false,
hasCI: false,
ciStatus: null,
sha: '12345678',
@@ -376,11 +377,29 @@ describe('ReadyToMerge', () => {
});
describe('initiateMergePolling', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
it('should call simplePoll', () => {
const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll');
vm.initiateMergePolling();
- expect(simplePoll).toHaveBeenCalled();
+ expect(simplePoll).toHaveBeenCalledWith(jasmine.any(Function), { timeout: 0 });
+ });
+
+ it('should call handleMergePolling', () => {
+ spyOn(vm, 'handleMergePolling');
+
+ vm.initiateMergePolling();
+
+ jasmine.clock().tick(2000);
+
+ expect(vm.handleMergePolling).toHaveBeenCalled();
});
});
@@ -624,6 +643,10 @@ describe('ReadyToMerge', () => {
const findCommitsHeaderElement = () => wrapper.find(CommitsHeader);
const findCommitEditElements = () => wrapper.findAll(CommitEdit);
const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
+ const findFirstCommitEditLabel = () =>
+ findCommitEditElements()
+ .at(0)
+ .props('label');
describe('squash checkbox', () => {
it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => {
@@ -648,31 +671,129 @@ describe('ReadyToMerge', () => {
});
describe('commits count collapsible header', () => {
- it('should be rendered if fast-forward is disabled', () => {
+ it('should be rendered when fast-forward is disabled', () => {
createLocalComponent();
expect(findCommitsHeaderElement().exists()).toBeTruthy();
});
- it('should not be rendered if fast-forward is enabled', () => {
- createLocalComponent({ mr: { ffOnlyEnabled: true } });
+ describe('when fast-forward is enabled', () => {
+ it('should be rendered if squash and squash before are enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ enableSquashBeforeMerge: true,
+ squash: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitsHeaderElement().exists()).toBeTruthy();
+ });
+
+ it('should not be rendered if squash before merge is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ enableSquashBeforeMerge: false,
+ squash: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ });
+
+ it('should not be rendered if squash is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: false,
+ enableSquashBeforeMerge: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ });
+
+ it('should not be rendered if commits count is 1', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ commitsCount: 1,
+ },
+ });
- expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ });
});
});
describe('commits edit components', () => {
+ describe('when fast-forward merge is enabled', () => {
+ it('should not be rendered if squash is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: false,
+ enableSquashBeforeMerge: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(0);
+ });
+
+ it('should not be rendered if squash before merge is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: false,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(0);
+ });
+
+ it('should not be rendered if there is only one commit', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ commitsCount: 1,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(0);
+ });
+
+ it('should have one edit component if squash is enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(1);
+ expect(findFirstCommitEditLabel()).toBe('Squash commit message');
+ });
+ });
+
it('should have one edit component when squash is disabled', () => {
createLocalComponent();
expect(findCommitEditElements().length).toBe(1);
});
- const findFirstCommitEditLabel = () =>
- findCommitEditElements()
- .at(0)
- .props('label');
-
it('should have two edit components when squash is enabled and there is more than 1 commit', () => {
createLocalComponent({
mr: {
diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
index bc71a90605a..e7ff9169f1b 100644
--- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
+++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
@@ -172,10 +172,8 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m
let(:exception) { ActiveRecord::RecordNotFound }
let(:perform_ignoring_exceptions) do
- begin
- subject.perform(start_id, stop_id)
- rescue described_class::Error
- end
+ subject.perform(start_id, stop_id)
+ rescue described_class::Error
end
before do
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index c432cc223b9..e1a2bae5fe8 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -95,6 +95,9 @@ describe Gitlab::BitbucketImport::Importer do
subject { described_class.new(project) }
describe '#import_pull_requests' do
+ let(:source_branch_sha) { sample.commits.last }
+ let(:target_branch_sha) { sample.commits.first }
+
before do
allow(subject).to receive(:import_wiki)
allow(subject).to receive(:import_issues)
@@ -102,9 +105,9 @@ describe Gitlab::BitbucketImport::Importer do
pull_request = instance_double(
Bitbucket::Representation::PullRequest,
iid: 10,
- source_branch_sha: sample.commits.last,
+ source_branch_sha: source_branch_sha,
source_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.source_branch,
- target_branch_sha: sample.commits.first,
+ target_branch_sha: target_branch_sha,
target_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.target_branch,
title: 'This is a title',
description: 'This is a test pull request',
@@ -162,6 +165,19 @@ describe Gitlab::BitbucketImport::Importer do
expect(reply_note).to be_a(DiffNote)
expect(reply_note.note).to eq(@reply.note)
end
+
+ context "when branches' sha is not found in the repository" do
+ let(:source_branch_sha) { 'a' * Commit::MIN_SHA_LENGTH }
+ let(:target_branch_sha) { 'b' * Commit::MIN_SHA_LENGTH }
+
+ it 'uses the pull request sha references' do
+ expect { subject.execute }.to change { MergeRequest.count }.by(1)
+
+ merge_request_diff = MergeRequest.first.merge_request_diff
+ expect(merge_request_diff.head_commit_sha).to eq source_branch_sha
+ expect(merge_request_diff.start_commit_sha).to eq target_branch_sha
+ end
+ end
end
context 'issues statuses' do
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
new file mode 100644
index 00000000000..4bc0a4c1398
--- /dev/null
+++ b/spec/lib/gitlab/danger/teammate_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+describe Gitlab::Danger::Teammate do
+ subject { described_class.new({ 'projects' => projects }) }
+ let(:projects) { { project => capabilities } }
+ let(:project) { double }
+
+ describe 'multiple roles project project' do
+ let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer database'] }
+
+ it '#reviewer? supports multiple roles per project' do
+ expect(subject.reviewer?(project, 'backend')).to be_truthy
+ end
+
+ it '#traintainer? supports multiple roles per project' do
+ expect(subject.traintainer?(project, 'database')).to be_truthy
+ end
+
+ it '#maintainer? supports multiple roles per project' do
+ expect(subject.maintainer?(project, 'frontend')).to be_truthy
+ end
+ end
+
+ describe 'one role project project' do
+ let(:capabilities) { 'reviewer backend' }
+
+ it '#reviewer? supports one role per project' do
+ expect(subject.reviewer?(project, 'backend')).to be_truthy
+ end
+
+ it '#traintainer? supports one role per project' do
+ expect(subject.traintainer?(project, 'database')).to be_falsey
+ end
+
+ it '#maintainer? supports one role per project' do
+ expect(subject.maintainer?(project, 'frontend')).to be_falsey
+ 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
index b44e8c5a110..bd3c66d0548 100644
--- a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
@@ -6,10 +6,11 @@ describe Gitlab::Database::Count::ReltuplesCountStrategy do
create(:identity)
end
- let(:models) { [Project, Identity] }
subject { described_class.new(models).count }
describe '#count', :postgresql do
+ let(:models) { [Project, Identity] }
+
context 'when reltuples is up to date' do
before do
ActiveRecord::Base.connection.execute('ANALYZE projects')
@@ -23,6 +24,22 @@ describe Gitlab::Database::Count::ReltuplesCountStrategy do
end
end
+ context 'when models using single-type inheritance are used' do
+ let(:models) { [Group, CiService, Namespace] }
+
+ before do
+ models.each do |model|
+ ActiveRecord::Base.connection.execute("ANALYZE #{model.table_name}")
+ end
+ end
+
+ it 'returns nil counts for inherited tables' do
+ models.each { |model| expect(model).not_to receive(:count) }
+
+ expect(subject).to eq({ Namespace => 3 })
+ end
+ end
+
context 'insufficient permissions' do
it 'returns an empty hash' do
allow(ActiveRecord::Base).to receive(:transaction).and_raise(PG::InsufficientPrivilege)
diff --git a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
index 203f9344a41..40d810b195b 100644
--- a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
@@ -4,15 +4,23 @@ describe Gitlab::Database::Count::TablesampleCountStrategy do
before do
create_list(:project, 3)
create(:identity)
+ create(:group)
end
- let(:models) { [Project, Identity] }
+ let(:models) { [Project, Identity, Group, Namespace] }
let(:strategy) { described_class.new(models) }
subject { strategy.count }
describe '#count', :postgresql do
- let(:estimates) { { Project => threshold + 1, Identity => threshold - 1 } }
+ let(:estimates) do
+ {
+ Project => threshold + 1,
+ Identity => threshold - 1,
+ Group => threshold + 1,
+ Namespace => threshold + 1
+ }
+ end
let(:threshold) { Gitlab::Database::Count::TablesampleCountStrategy::EXACT_COUNT_THRESHOLD }
before do
@@ -30,9 +38,13 @@ describe Gitlab::Database::Count::TablesampleCountStrategy do
context 'for tables with an estimated large size' do
it 'performs a tablesample count' do
expect(Project).not_to receive(:count)
+ expect(Group).not_to receive(:count)
+ expect(Namespace).not_to receive(:count)
result = subject
expect(result[Project]).to eq(3)
+ expect(result[Group]).to eq(1)
+ expect(result[Namespace]).to eq(4)
end
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 60106ee3c0b..ae50abd0e7a 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -87,6 +87,38 @@ describe Gitlab::Database do
end
end
+ describe '.postgresql_minimum_supported_version?' do
+ it 'returns false when not using PostgreSQL' do
+ allow(described_class).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.postgresql_minimum_supported_version?).to eq(false)
+ end
+
+ context 'when using PostgreSQL' do
+ before do
+ allow(described_class).to receive(:postgresql?).and_return(true)
+ end
+
+ it 'returns false when using PostgreSQL 9.5' do
+ allow(described_class).to receive(:version).and_return('9.5')
+
+ expect(described_class.postgresql_minimum_supported_version?).to eq(false)
+ end
+
+ it 'returns true when using PostgreSQL 9.6' do
+ allow(described_class).to receive(:version).and_return('9.6')
+
+ expect(described_class.postgresql_minimum_supported_version?).to eq(true)
+ end
+
+ it 'returns true when using PostgreSQL 10 or newer' do
+ allow(described_class).to receive(:version).and_return('10')
+
+ expect(described_class.postgresql_minimum_supported_version?).to eq(true)
+ end
+ end
+ end
+
describe '.join_lateral_supported?' do
it 'returns false when using MySQL' do
allow(described_class).to receive(:postgresql?).and_return(false)
@@ -195,6 +227,12 @@ describe Gitlab::Database do
end
end
+ describe '.pg_last_xact_replay_timestamp' do
+ it 'returns pg_last_xact_replay_timestamp' do
+ expect(described_class.pg_last_xact_replay_timestamp).to eq('pg_last_xact_replay_timestamp')
+ end
+ end
+
describe '.nulls_last_order' do
context 'when using PostgreSQL' do
before do
diff --git a/spec/lib/gitlab/git/repository_cleaner_spec.rb b/spec/lib/gitlab/git/repository_cleaner_spec.rb
index 7f9cc2bc9ec..6602f22843f 100644
--- a/spec/lib/gitlab/git/repository_cleaner_spec.rb
+++ b/spec/lib/gitlab/git/repository_cleaner_spec.rb
@@ -37,14 +37,12 @@ describe Gitlab::Git::RepositoryCleaner do
let(:object_map) { Gitlab::HttpIO.new(url, object_map_data.size) }
around do |example|
- begin
- tempfile.write(object_map_data)
- tempfile.close
+ tempfile.write(object_map_data)
+ tempfile.close
- example.run
- ensure
- tempfile.unlink
- end
+ example.run
+ ensure
+ tempfile.unlink
end
it 'removes internal references' do
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index cf12baf1a93..f1acb1d9bc4 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -149,11 +149,21 @@ describe Gitlab::GitalyClient do
end
end
- context 'when RequestStore is enabled', :request_store do
+ context 'when RequestStore is enabled and the maximum number of calls is not enforced by a feature flag', :request_store do
+ before do
+ stub_feature_flags(gitaly_enforce_requests_limits: false)
+ end
+
it 'allows up the maximum number of allowed calls' do
expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error
end
+ it 'allows the maximum number of calls to be exceeded if GITALY_DISABLE_REQUEST_LIMITS is set' do
+ stub_env('GITALY_DISABLE_REQUEST_LIMITS', 'true')
+
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.not_to raise_error
+ end
+
context 'when the maximum number of calls has been reached' do
before do
call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS)
@@ -189,6 +199,32 @@ describe Gitlab::GitalyClient do
end
end
+ context 'in production and when RequestStore is enabled', :request_store do
+ before do
+ allow(Rails.env).to receive(:production?).and_return(true)
+ end
+
+ context 'when the maximum number of calls is enforced by a feature flag' do
+ before do
+ stub_feature_flags(gitaly_enforce_requests_limits: true)
+ end
+
+ it 'does not allow the maximum number of calls to be exceeded' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError)
+ end
+ end
+
+ context 'when the maximum number of calls is not enforced by a feature flag' do
+ before do
+ stub_feature_flags(gitaly_enforce_requests_limits: false)
+ end
+
+ it 'allows the maximum number of calls to be exceeded' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.not_to raise_error
+ end
+ end
+ end
+
context 'when RequestStore is not active' do
it 'does not raise errors when the maximum number of allowed calls is exceeded' do
expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 2) }.not_to raise_error
diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
index 6154b3e2f76..d03a74ac9eb 100644
--- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb
+++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe Gitlab::HashedStorage::Migrator do
+describe Gitlab::HashedStorage::Migrator, :sidekiq do
describe '#bulk_schedule_migration' do
it 'schedules job to HashedStorage::MigratorWorker' do
Sidekiq::Testing.fake! do
@@ -182,4 +184,52 @@ describe Gitlab::HashedStorage::Migrator do
end
end
end
+
+ describe 'migration_pending?' do
+ set(:project) { create(:project, :empty_repo) }
+
+ it 'returns true when there are MigratorWorker jobs scheduled' do
+ Sidekiq::Testing.fake! do
+ ::HashedStorage::MigratorWorker.perform_async(1, 5)
+
+ expect(subject.migration_pending?).to be_truthy
+ end
+ end
+
+ it 'returns true when there are ProjectMigrateWorker jobs scheduled' do
+ Sidekiq::Testing.fake! do
+ ::HashedStorage::ProjectMigrateWorker.perform_async(1)
+
+ expect(subject.migration_pending?).to be_truthy
+ end
+ end
+
+ it 'returns false when queues are empty' do
+ expect(subject.migration_pending?).to be_falsey
+ end
+ end
+
+ describe 'rollback_pending?' do
+ set(:project) { create(:project, :empty_repo) }
+
+ it 'returns true when there are RollbackerWorker jobs scheduled' do
+ Sidekiq::Testing.fake! do
+ ::HashedStorage::RollbackerWorker.perform_async(1, 5)
+
+ expect(subject.rollback_pending?).to be_truthy
+ end
+ end
+
+ it 'returns true when there are jobs scheduled' do
+ Sidekiq::Testing.fake! do
+ ::HashedStorage::ProjectRollbackWorker.perform_async(1)
+
+ expect(subject.rollback_pending?).to be_truthy
+ end
+ end
+
+ it 'returns false when queues are empty' do
+ expect(subject.rollback_pending?).to be_falsey
+ end
+ end
end
diff --git a/spec/lib/gitlab/middleware/basic_health_check_spec.rb b/spec/lib/gitlab/middleware/basic_health_check_spec.rb
index 187d903a5e1..86bdc479b66 100644
--- a/spec/lib/gitlab/middleware/basic_health_check_spec.rb
+++ b/spec/lib/gitlab/middleware/basic_health_check_spec.rb
@@ -28,6 +28,35 @@ describe Gitlab::Middleware::BasicHealthCheck do
end
end
+ context 'with X-Forwarded-For headers' do
+ let(:load_balancer_ip) { '1.2.3.4' }
+
+ before do
+ env['HTTP_X_FORWARDED_FOR'] = "#{load_balancer_ip}, 127.0.0.1"
+ env['REMOTE_ADDR'] = '127.0.0.1'
+ env['PATH_INFO'] = described_class::HEALTH_PATH
+ end
+
+ it 'returns 200 response when endpoint is allowed' do
+ allow(Settings.monitoring).to receive(:ip_whitelist).and_return([load_balancer_ip])
+ expect(app).not_to receive(:call)
+
+ response = middleware.call(env)
+
+ expect(response[0]).to eq(200)
+ expect(response[1]).to eq({ 'Content-Type' => 'text/plain' })
+ expect(response[2]).to eq(['GitLab OK'])
+ end
+
+ it 'returns 404 when whitelist is not configured' do
+ allow(Settings.monitoring).to receive(:ip_whitelist).and_return([])
+
+ response = middleware.call(env)
+
+ expect(response[0]).to eq(404)
+ end
+ end
+
context 'whitelisted IP' do
before do
env['REMOTE_ADDR'] = '127.0.0.1'
diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb
index fd443cc1f71..23e45aff1c5 100644
--- a/spec/lib/gitlab/request_context_spec.rb
+++ b/spec/lib/gitlab/request_context_spec.rb
@@ -6,6 +6,31 @@ describe Gitlab::RequestContext do
let(:app) { -> (env) {} }
let(:env) { Hash.new }
+ context 'with X-Forwarded-For headers', :request_store do
+ let(:load_balancer_ip) { '1.2.3.4' }
+ let(:headers) do
+ {
+ 'HTTP_X_FORWARDED_FOR' => "#{load_balancer_ip}, 127.0.0.1",
+ 'REMOTE_ADDR' => '127.0.0.1'
+ }
+ end
+
+ let(:env) { Rack::MockRequest.env_for("/").merge(headers) }
+
+ it 'returns the load balancer IP' do
+ client_ip = nil
+
+ endpoint = proc do
+ client_ip = Gitlab::SafeRequestStore[:client_ip]
+ [200, {}, ["Hello"]]
+ end
+
+ described_class.new(endpoint).call(env)
+
+ expect(client_ip).to eq(load_balancer_ip)
+ end
+ end
+
context 'when RequestStore::Middleware is used' do
around do |example|
RequestStore::Middleware.new(-> (env) { example.run }).call({})
@@ -15,7 +40,7 @@ describe Gitlab::RequestContext do
let(:ip) { '192.168.1.11' }
before do
- allow_any_instance_of(ActionDispatch::Request).to receive(:ip).and_return(ip)
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
described_class.new(app).call(env)
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
index ff8c0825ee4..1a5a38b5d99 100644
--- a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
@@ -54,7 +54,7 @@ describe Gitlab::SidekiqMiddleware::MemoryKiller do
expect(Process).to receive(:kill).with('SIGTSTP', pid).ordered
expect(Process).to receive(:kill).with('SIGTERM', pid).ordered
- expect(Process).to receive(:kill).with('SIGKILL', "-#{pid}").ordered
+ expect(Process).to receive(:kill).with('SIGKILL', 0).ordered
run
end
diff --git a/spec/lib/gitlab/user_extractor_spec.rb b/spec/lib/gitlab/user_extractor_spec.rb
index fcc05ab3a0c..6e2bb81fbda 100644
--- a/spec/lib/gitlab/user_extractor_spec.rb
+++ b/spec/lib/gitlab/user_extractor_spec.rb
@@ -48,6 +48,14 @@ describe Gitlab::UserExtractor do
it 'includes all mentioned usernames' do
expect(extractor.matches[:usernames]).to contain_exactly('user-1', 'user-2', 'user-4')
end
+
+ context 'input has no matching e-mail or usernames' do
+ it 'returns an empty list of users' do
+ extractor = described_class.new('My test')
+
+ expect(extractor.users).to be_empty
+ end
+ end
end
describe '#references' do
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index e2134dc279c..1fefc947636 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -97,6 +97,12 @@ describe GoogleApi::CloudPlatform::Client do
"node_config": {
"machine_type": machine_type
},
+ "master_auth": {
+ "username": "admin",
+ "client_certificate_config": {
+ issue_client_certificate: true
+ }
+ },
"legacy_abac": {
"enabled": true
}
@@ -122,6 +128,12 @@ describe GoogleApi::CloudPlatform::Client do
"node_config": {
"machine_type": machine_type
},
+ "master_auth": {
+ "username": "admin",
+ "client_certificate_config": {
+ issue_client_certificate: true
+ }
+ },
"legacy_abac": {
"enabled": false
}
diff --git a/spec/migrations/add_foreign_keys_to_todos_spec.rb b/spec/migrations/add_foreign_keys_to_todos_spec.rb
index efd87173b9c..2500e2f8333 100644
--- a/spec/migrations/add_foreign_keys_to_todos_spec.rb
+++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb
@@ -36,7 +36,7 @@ describe AddForeignKeysToTodos, :migration do
end
context 'add foreign key on note_id' do
- let(:note) { create(:note) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let(:note) { table(:notes).create! }
let!(:todo_with_note) { create_todo(note_id: note.id) }
let!(:todo_with_invalid_note) { create_todo(note_id: 4711) }
let!(:todo_without_note) { create_todo(note_id: nil) }
diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
index d8dd7a2fb83..13dc62595b5 100644
--- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
+++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
@@ -1,23 +1,21 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb')
-describe AddHeadPipelineForEachMergeRequest, :delete do
- include ProjectForksHelper
-
+describe AddHeadPipelineForEachMergeRequest, :migration do
let(:migration) { described_class.new }
- let!(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:other_project) { fork_project(project) }
+ let!(:project) { table(:projects).create! }
+ let!(:other_project) { table(:projects).create! }
- let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: "branch_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:pipeline_2) { create(:ci_pipeline, project: other_project, ref: "branch_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:pipeline_3) { create(:ci_pipeline, project: other_project, ref: "branch_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:pipeline_4) { create(:ci_pipeline, project: project, ref: "branch_2") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:pipeline_1) { table(:ci_pipelines).create!(project_id: project.id, ref: "branch_1") }
+ let!(:pipeline_2) { table(:ci_pipelines).create!(project_id: other_project.id, ref: "branch_1") }
+ let!(:pipeline_3) { table(:ci_pipelines).create!(project_id: other_project.id, ref: "branch_1") }
+ let!(:pipeline_4) { table(:ci_pipelines).create!(project_id: project.id, ref: "branch_2") }
- let!(:mr_1) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_1", target_branch: "target_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:mr_2) { create(:merge_request, source_project: other_project, target_project: project, source_branch: "branch_1", target_branch: "target_2") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:mr_3) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_2", target_branch: "master") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:mr_4) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_3", target_branch: "master") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:mr_1) { table(:merge_requests).create!(source_project_id: project.id, target_project_id: project.id, source_branch: "branch_1", target_branch: "target_1") }
+ let!(:mr_2) { table(:merge_requests).create!(source_project_id: other_project.id, target_project_id: project.id, source_branch: "branch_1", target_branch: "target_2") }
+ let!(:mr_3) { table(:merge_requests).create!(source_project_id: project.id, target_project_id: project.id, source_branch: "branch_2", target_branch: "master") }
+ let!(:mr_4) { table(:merge_requests).create!(source_project_id: project.id, target_project_id: project.id, source_branch: "branch_3", target_branch: "master") }
context "#up" do
context "when source_project and source_branch of pipeline are the same of merge request" do
diff --git a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
index 19f06810e54..09c78d02890 100644
--- a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
+++ b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
@@ -3,12 +3,30 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170803090603_calculate_conv_dev_index_percentages.rb')
-describe CalculateConvDevIndexPercentages, :delete do
+describe CalculateConvDevIndexPercentages, :migration do
let(:migration) { described_class.new }
let!(:conv_dev_index) do
- create(:conversational_development_index_metric, # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ table(:conversational_development_index_metrics).create!(
+ leader_issues: 9.256,
leader_notes: 0,
+ leader_milestones: 16.2456,
+ leader_boards: 5.2123,
+ leader_merge_requests: 1.2,
+ leader_ci_pipelines: 12.1234,
+ leader_environments: 3.3333,
+ leader_deployments: 1.200,
+ leader_projects_prometheus_active: 0.111,
+ leader_service_desk_issues: 15.891,
+ instance_issues: 1.234,
+ instance_notes: 28.123,
instance_milestones: 0,
+ instance_boards: 3.254,
+ instance_merge_requests: 0.6,
+ instance_ci_pipelines: 2.344,
+ instance_environments: 2.2222,
+ instance_deployments: 0.771,
+ instance_projects_prometheus_active: 0.109,
+ instance_service_desk_issues: 13.345,
percentage_issues: 0,
percentage_notes: 0,
percentage_milestones: 0,
diff --git a/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb b/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb
index 8f40ac3e38b..0e6bded29b4 100644
--- a/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb
+++ b/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb
@@ -1,20 +1,17 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170816102555_cleanup_nonexisting_namespace_pending_delete_projects.rb')
-describe CleanupNonexistingNamespacePendingDeleteProjects do
- before do
- # Stub after_save callbacks that will fail when Project has invalid namespace
- allow_any_instance_of(Project).to receive(:ensure_storage_path_exist).and_return(nil)
- allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
- end
+describe CleanupNonexistingNamespacePendingDeleteProjects, :migration do
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
describe '#up' do
- set(:some_project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:some_project) { projects.create! }
+ let(:namespace) { namespaces.create!(name: 'test', path: 'test') }
it 'only cleans up when namespace does not exist' do
- create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs
- project = build(:project, pending_delete: true, namespace: nil, namespace_id: Namespace.maximum(:id).to_i.succ) # rubocop:disable RSpec/FactoriesInMigrationSpecs
- project.save(validate: false)
+ projects.create!(pending_delete: true, namespace_id: namespace.id)
+ project = projects.create!(pending_delete: true, namespace_id: 0)
expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]])
@@ -22,7 +19,7 @@ describe CleanupNonexistingNamespacePendingDeleteProjects do
end
it 'does nothing when no pending delete projects without namespace found' do
- create(:project, pending_delete: true, namespace: create(:namespace)) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ projects.create!(pending_delete: true, namespace_id: namespace.id)
expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async)
diff --git a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
index e2ce69a7bb1..58b8b4a16f0 100644
--- a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
+++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
@@ -1,25 +1,36 @@
# frozen_string_literal: true
-# rubocop:disable RSpec/FactoriesInMigrationSpecs
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180723130817_delete_inconsistent_internal_id_records.rb')
describe DeleteInconsistentInternalIdRecords, :migration do
- let!(:project1) { create(:project) }
- let!(:project2) { create(:project) }
- let!(:project3) { create(:project) }
+ let!(:namespace) { table(:namespaces).create!(name: 'test', path: 'test') }
+ let!(:project1) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:project2) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:project3) { table(:projects).create!(namespace_id: namespace.id) }
- let(:internal_id_query) { ->(project) { InternalId.where(usage: InternalId.usages[scope.to_s.tableize], project: project) } }
+ let(:internal_ids) { table(:internal_ids) }
+ let(:internal_id_query) { ->(project) { InternalId.where(usage: InternalId.usages[scope.to_s.tableize], project_id: project.id) } }
let(:create_models) do
- 3.times { create(scope, project: project1) }
- 3.times { create(scope, project: project2) }
- 3.times { create(scope, project: project3) }
+ [project1, project2, project3].each do |project|
+ 3.times do |i|
+ attributes = required_attributes.merge(project_id: project.id,
+ iid: i.succ)
+
+ table(scope.to_s.pluralize).create!(attributes)
+ end
+ end
end
shared_examples_for 'deleting inconsistent internal_id records' do
before do
create_models
+ [project1, project2, project3].each do |project|
+ internal_ids.create!(project_id: project.id, usage: InternalId.usages[scope.to_s.tableize], last_value: 3)
+ end
+
internal_id_query.call(project1).first.tap do |iid|
iid.last_value = iid.last_value - 2
# This is an inconsistent record
@@ -33,11 +44,11 @@ describe DeleteInconsistentInternalIdRecords, :migration do
end
end
- it "deletes inconsistent issues" do
+ it "deletes inconsistent records" do
expect { migrate! }.to change { internal_id_query.call(project1).size }.from(1).to(0)
end
- it "retains consistent issues" do
+ it "retains consistent records" do
expect { migrate! }.not_to change { internal_id_query.call(project2).size }
end
@@ -48,6 +59,8 @@ describe DeleteInconsistentInternalIdRecords, :migration do
context 'for issues' do
let(:scope) { :issue }
+ let(:required_attributes) { {} }
+
it_behaves_like 'deleting inconsistent internal_id records'
end
@@ -55,9 +68,17 @@ describe DeleteInconsistentInternalIdRecords, :migration do
let(:scope) { :merge_request }
let(:create_models) do
- 3.times { |i| create(scope, target_project: project1, source_project: project1, source_branch: i.to_s) }
- 3.times { |i| create(scope, target_project: project2, source_project: project2, source_branch: i.to_s) }
- 3.times { |i| create(scope, target_project: project3, source_project: project3, source_branch: i.to_s) }
+ [project1, project2, project3].each do |project|
+ 3.times do |i|
+ table(:merge_requests).create!(
+ target_project_id: project.id,
+ source_project_id: project.id,
+ target_branch: 'master',
+ source_branch: j.to_s,
+ iid: i.succ
+ )
+ end
+ end
end
it_behaves_like 'deleting inconsistent internal_id records'
@@ -66,13 +87,6 @@ describe DeleteInconsistentInternalIdRecords, :migration do
context 'for deployments' do
let(:scope) { :deployment }
let(:deployments) { table(:deployments) }
- let(:internal_ids) { table(:internal_ids) }
-
- before do
- internal_ids.create!(project_id: project1.id, usage: 2, last_value: 2)
- internal_ids.create!(project_id: project2.id, usage: 2, last_value: 2)
- internal_ids.create!(project_id: project3.id, usage: 2, last_value: 2)
- end
let(:create_models) do
3.times { |i| deployments.create!(project_id: project1.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) }
@@ -85,17 +99,14 @@ describe DeleteInconsistentInternalIdRecords, :migration do
context 'for milestones (by project)' do
let(:scope) { :milestone }
+ let(:required_attributes) { { title: 'test' } }
+
it_behaves_like 'deleting inconsistent internal_id records'
end
context 'for ci_pipelines' do
let(:scope) { :ci_pipeline }
-
- let(:create_models) do
- create_list(:ci_empty_pipeline, 3, project: project1)
- create_list(:ci_empty_pipeline, 3, project: project2)
- create_list(:ci_empty_pipeline, 3, project: project3)
- end
+ let(:required_attributes) { { ref: 'test' } }
it_behaves_like 'deleting inconsistent internal_id records'
end
@@ -107,12 +118,20 @@ describe DeleteInconsistentInternalIdRecords, :migration do
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) } }
+ let(:internal_id_query) { ->(group) { InternalId.where(usage: InternalId.usages['milestones'], namespace_id: group.id) } }
before do
- 3.times { create(:milestone, group_id: group1.id) }
- 3.times { create(:milestone, group_id: group2.id) }
- 3.times { create(:milestone, group_id: group3.id) }
+ [group1, group2, group3].each do |group|
+ 3.times do |i|
+ table(:milestones).create!(
+ group_id: group.id,
+ title: 'test',
+ iid: i.succ
+ )
+ end
+
+ internal_ids.create!(namespace_id: group.id, usage: InternalId.usages['milestones'], last_value: 3)
+ end
internal_id_query.call(group1).first.tap do |iid|
iid.last_value = iid.last_value - 2
@@ -127,11 +146,11 @@ describe DeleteInconsistentInternalIdRecords, :migration do
end
end
- it "deletes inconsistent issues" do
+ it "deletes inconsistent records" do
expect { migrate! }.to change { internal_id_query.call(group1).size }.from(1).to(0)
end
- it "retains consistent issues" do
+ it "retains consistent records" do
expect { migrate! }.not_to change { internal_id_query.call(group2).size }
end
diff --git a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
index 495e86ee888..71a4e71ac8a 100644
--- a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
+++ b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
@@ -1,20 +1,19 @@
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20171106151218_issues_moved_to_id_foreign_key.rb')
-# The schema version has to be far enough in advance to have the
-# only_mirror_protected_branches column in the projects table to create a
-# project via FactoryBot.
-describe IssuesMovedToIdForeignKey, :migration, schema: 20171114150259 do
- let!(:issue_first) { create(:issue, moved_to_id: issue_second.id) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:issue_second) { create(:issue, moved_to_id: issue_third.id) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:issue_third) { create(:issue) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+describe IssuesMovedToIdForeignKey, :migration do
+ let(:issues) { table(:issues) }
+
+ let!(:issue_third) { issues.create! }
+ let!(:issue_second) { issues.create!(moved_to_id: issue_third.id) }
+ let!(:issue_first) { issues.create!(moved_to_id: issue_second.id) }
subject { described_class.new }
it 'removes the orphaned moved_to_id' do
subject.down
- issue_third.update(moved_to_id: 100000)
+ issue_third.update!(moved_to_id: 0)
subject.up
diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb
index af77d64fdbf..79e21514506 100644
--- a/spec/migrations/migrate_old_artifacts_spec.rb
+++ b/spec/migrations/migrate_old_artifacts_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170523083112_migrate_old_artifacts.rb')
-describe MigrateOldArtifacts do
+# Adding the ci_job_artifacts table (from the 20170918072948 schema)
+# makes the use of model code below easier.
+describe MigrateOldArtifacts, :migration, schema: 20170918072948 do
let(:migration) { described_class.new }
let!(:directory) { Dir.mktmpdir }
@@ -16,18 +18,22 @@ describe MigrateOldArtifacts do
end
context 'with migratable data' do
- set(:project1) { create(:project, ci_id: 2) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- set(:project2) { create(:project, ci_id: 3) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- set(:project3) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
-
- set(:pipeline1) { create(:ci_empty_pipeline, project: project1) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- set(:pipeline2) { create(:ci_empty_pipeline, project: project2) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- set(:pipeline3) { create(:ci_empty_pipeline, project: project3) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
-
- let!(:build_with_legacy_artifacts) { create(:ci_build, pipeline: pipeline1) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:build_without_artifacts) { create(:ci_build, pipeline: pipeline1) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:build2) { create(:ci_build, pipeline: pipeline2) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:build3) { create(:ci_build, pipeline: pipeline3) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let(:projects) { table(:projects) }
+ let(:ci_pipelines) { table(:ci_pipelines) }
+ let(:ci_builds) { table(:ci_builds) }
+
+ let!(:project1) { projects.create!(ci_id: 2) }
+ let!(:project2) { projects.create!(ci_id: 3) }
+ let!(:project3) { projects.create! }
+
+ let!(:pipeline1) { ci_pipelines.create!(project_id: project1.id) }
+ let!(:pipeline2) { ci_pipelines.create!(project_id: project2.id) }
+ let!(:pipeline3) { ci_pipelines.create!(project_id: project3.id) }
+
+ let!(:build_with_legacy_artifacts) { ci_builds.create!(commit_id: pipeline1.id, project_id: project1.id, type: 'Ci::Build').becomes(Ci::Build) }
+ let!(:build_without_artifacts) { ci_builds.create!(commit_id: pipeline1.id, project_id: project1.id, type: 'Ci::Build').becomes(Ci::Build) }
+ let!(:build2) { ci_builds.create!(commit_id: pipeline2.id, project_id: project2.id, type: 'Ci::Build').becomes(Ci::Build) }
+ let!(:build3) { ci_builds.create!(commit_id: pipeline3.id, project_id: project3.id, type: 'Ci::Build').becomes(Ci::Build) }
before do
setup_builds(build2, build3)
diff --git a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb
index 99173708190..88aef3b70b4 100644
--- a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb
+++ b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170324160416_migrate_user_activities_to_users_last_activity_on.rb')
-describe MigrateUserActivitiesToUsersLastActivityOn, :clean_gitlab_redis_shared_state, :delete do
+describe MigrateUserActivitiesToUsersLastActivityOn, :clean_gitlab_redis_shared_state, :migration do
let(:migration) { described_class.new }
- let!(:user_active_1) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:user_active_2) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:user_active_1) { table(:users).create!(email: 'test1', username: 'test1') }
+ let!(:user_active_2) { table(:users).create!(email: 'test2', username: 'test2') }
def record_activity(user, time)
Gitlab::Redis::SharedState.with do |redis|
diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb
index 80468b9d01e..a0179ab3ceb 100644
--- a/spec/migrations/migrate_user_project_view_spec.rb
+++ b/spec/migrations/migrate_user_project_view_spec.rb
@@ -3,15 +3,15 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_project_view.rb')
-describe MigrateUserProjectView, :delete do
+describe MigrateUserProjectView, :migration do
let(:migration) { described_class.new }
- let!(:user) { create(:user, project_view: 'readme') } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:user) { table(:users).create!(project_view: User.project_views['readme']) }
describe '#up' do
it 'updates project view setting with new value' do
migration.up
- expect(user.reload.project_view).to eq('files')
+ expect(user.reload.project_view).to eq(User.project_views['files'])
end
end
end
diff --git a/spec/migrations/move_personal_snippets_files_spec.rb b/spec/migrations/move_personal_snippets_files_spec.rb
index 1f39ad98fb8..d94ae1e52f5 100644
--- a/spec/migrations/move_personal_snippets_files_spec.rb
+++ b/spec/migrations/move_personal_snippets_files_spec.rb
@@ -1,12 +1,19 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170612071012_move_personal_snippets_files.rb')
-describe MovePersonalSnippetsFiles do
+describe MovePersonalSnippetsFiles, :migration do
let(:migration) { described_class.new }
let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_snippet_files_test") }
let(:uploads_dir) { File.join(test_dir, 'uploads') }
let(:new_uploads_dir) { File.join(uploads_dir, '-', 'system') }
+ let(:notes) { table(:notes) }
+ let(:snippets) { table(:snippets) }
+ let(:uploads) { table(:uploads) }
+
+ let(:user) { table(:users).create!(email: 'user@example.com', projects_limit: 10) }
+ let(:project) { table(:projects).create!(name: 'gitlab', namespace_id: 1) }
+
before do
allow(CarrierWave).to receive(:root).and_return(test_dir)
allow(migration).to receive(:base_directory).and_return(test_dir)
@@ -16,14 +23,14 @@ describe MovePersonalSnippetsFiles do
describe "#up" do
let(:snippet) do
- snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ snippet = snippets.create!(author_id: user.id)
create_upload('picture.jpg', snippet)
snippet.update(description: markdown_linking_file('picture.jpg', snippet))
snippet
end
let(:snippet_with_missing_file) do
- snippet = create(:snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ snippet = snippets.create!(author_id: user.id, project_id: project.id)
create_upload('picture.jpg', snippet, create_file: false)
snippet.update(description: markdown_linking_file('picture.jpg', snippet))
snippet
@@ -62,7 +69,10 @@ describe MovePersonalSnippetsFiles do
secret = "secret#{snippet.id}"
file_location = "/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg"
markdown = markdown_linking_file('picture.jpg', snippet)
- note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ note = notes.create!(noteable_id: snippet.id,
+ noteable_type: Snippet,
+ note: "with #{markdown}",
+ author_id: user.id)
migration.up
@@ -73,14 +83,14 @@ describe MovePersonalSnippetsFiles do
describe "#down" do
let(:snippet) do
- snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ snippet = snippets.create!(author_id: user.id)
create_upload('picture.jpg', snippet, in_new_path: true)
snippet.update(description: markdown_linking_file('picture.jpg', snippet, in_new_path: true))
snippet
end
let(:snippet_with_missing_file) do
- snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ snippet = snippets.create!(author_id: user.id)
create_upload('picture.jpg', snippet, create_file: false, in_new_path: true)
snippet.update(description: markdown_linking_file('picture.jpg', snippet, in_new_path: true))
snippet
@@ -119,7 +129,10 @@ describe MovePersonalSnippetsFiles do
markdown = markdown_linking_file('picture.jpg', snippet, in_new_path: true)
secret = "secret#{snippet.id}"
file_location = "/uploads/personal_snippet/#{snippet.id}/#{secret}/picture.jpg"
- note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ note = notes.create!(noteable_id: snippet.id,
+ noteable_type: Snippet,
+ note: "with #{markdown}",
+ author_id: user.id)
migration.down
@@ -135,7 +148,7 @@ describe MovePersonalSnippetsFiles do
secret = '123456789'
filename = 'hello.jpg'
- snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ snippet = snippets.create!(author_id: user.id)
path_before = "/uploads/personal_snippet/#{snippet.id}/#{secret}/#{filename}"
path_after = "/uploads/system/personal_snippet/#{snippet.id}/#{secret}/#{filename}"
@@ -161,7 +174,11 @@ describe MovePersonalSnippetsFiles do
FileUtils.touch(absolute_path)
end
- create(:upload, model: snippet, path: "#{secret}/#{filename}", uploader: PersonalFileUploader) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ uploads.create!(model_id: snippet.id,
+ model_type: snippet.class,
+ path: "#{secret}/#{filename}",
+ uploader: PersonalFileUploader,
+ size: 100.kilobytes)
end
def markdown_linking_file(filename, snippet, in_new_path: false)
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index dd40b968bc1..9ca4241d7d8 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2772,7 +2772,7 @@ describe Ci::Build do
end
context 'when ref is merge request' do
- let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
let(:pipeline) { merge_request.merge_request_pipelines.first }
let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) }
@@ -2830,7 +2830,7 @@ describe Ci::Build do
end
context 'when ref is merge request' do
- let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
let(:pipeline) { merge_request.merge_request_pipelines.first }
let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index d0b42d103a5..7eeaa7a18ef 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -130,22 +130,118 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '.for_sha' do
+ subject { described_class.for_sha(sha) }
+
+ let(:sha) { 'abc' }
+ let!(:pipeline) { create(:ci_pipeline, sha: 'abc') }
+
+ it 'returns the pipeline' do
+ is_expected.to contain_exactly(pipeline)
+ end
+
+ context 'when argument is array' do
+ let(:sha) { %w[abc def] }
+ let!(:pipeline_2) { create(:ci_pipeline, sha: 'def') }
+
+ it 'returns the pipelines' do
+ is_expected.to contain_exactly(pipeline, pipeline_2)
+ end
+ end
+
+ context 'when sha is empty' do
+ let(:sha) { nil }
+
+ it 'does not return anything' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.for_source_sha' do
+ subject { described_class.for_source_sha(source_sha) }
+
+ let(:source_sha) { 'abc' }
+ let!(:pipeline) { create(:ci_pipeline, source_sha: 'abc') }
+
+ it 'returns the pipeline' do
+ is_expected.to contain_exactly(pipeline)
+ end
+
+ context 'when argument is array' do
+ let(:source_sha) { %w[abc def] }
+ let!(:pipeline_2) { create(:ci_pipeline, source_sha: 'def') }
+
+ it 'returns the pipelines' do
+ is_expected.to contain_exactly(pipeline, pipeline_2)
+ end
+ end
+
+ context 'when source_sha is empty' do
+ let(:source_sha) { nil }
+
+ it 'does not return anything' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.for_sha_or_source_sha' do
+ subject { described_class.for_sha_or_source_sha(sha) }
+
+ let(:sha) { 'abc' }
+
+ context 'when sha is matched' do
+ let!(:pipeline) { create(:ci_pipeline, sha: sha) }
+
+ it 'returns the pipeline' do
+ is_expected.to contain_exactly(pipeline)
+ end
+ end
+
+ context 'when source sha is matched' do
+ let!(:pipeline) { create(:ci_pipeline, source_sha: sha) }
+
+ it 'returns the pipeline' do
+ is_expected.to contain_exactly(pipeline)
+ end
+ end
+
+ context 'when both sha and source sha are not matched' do
+ let!(:pipeline) { create(:ci_pipeline, sha: 'bcd', source_sha: 'bcd') }
+
+ it 'does not return anything' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.detached_merge_request_pipelines' do
- subject { described_class.detached_merge_request_pipelines(merge_request) }
+ subject { described_class.detached_merge_request_pipelines(merge_request, sha) }
let!(:pipeline) do
- create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, target_sha: target_sha)
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, sha: merge_request.diff_head_sha)
end
let(:merge_request) { create(:merge_request) }
- let(:target_sha) { nil }
+ let(:sha) { merge_request.diff_head_sha }
it 'returns detached merge request pipelines' do
is_expected.to eq([pipeline])
end
- context 'when target sha exists' do
- let(:target_sha) { merge_request.target_branch_sha }
+ context 'when sha does not exist' do
+ let(:sha) { 'abc' }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when pipeline is merge request pipeline' do
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, source_sha: merge_request.diff_head_sha)
+ end
it 'returns empty array' do
is_expected.to be_empty
@@ -173,21 +269,31 @@ describe Ci::Pipeline, :mailer do
end
describe '.merge_request_pipelines' do
- subject { described_class.merge_request_pipelines(merge_request) }
+ subject { described_class.merge_request_pipelines(merge_request, source_sha) }
let!(:pipeline) do
- create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, target_sha: target_sha)
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, source_sha: merge_request.diff_head_sha)
end
let(:merge_request) { create(:merge_request) }
- let(:target_sha) { merge_request.target_branch_sha }
+ let(:source_sha) { merge_request.diff_head_sha }
it 'returns merge pipelines' do
is_expected.to eq([pipeline])
end
- context 'when target sha is empty' do
- let(:target_sha) { nil }
+ context 'when source sha is empty' do
+ let(:source_sha) { nil }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when pipeline is detached merge request pipeline' do
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, sha: merge_request.diff_head_sha)
+ end
it 'returns empty array' do
is_expected.to be_empty
@@ -256,6 +362,50 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '.triggered_for_branch' do
+ subject { described_class.triggered_for_branch(ref) }
+
+ let(:project) { create(:project, :repository) }
+ let(:ref) { 'feature' }
+ let!(:pipeline) { create(:ci_pipeline, ref: ref) }
+
+ it 'returns the pipeline' do
+ is_expected.to eq([pipeline])
+ end
+
+ context 'when sha is not specified' do
+ it 'returns the pipeline' do
+ expect(described_class.triggered_for_branch(ref)).to eq([pipeline])
+ end
+ end
+
+ context 'when pipeline is triggered for tag' do
+ let(:ref) { 'v1.1.0' }
+ let!(:pipeline) { create(:ci_pipeline, ref: ref, tag: true) }
+
+ it 'does not return the pipeline' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when pipeline is triggered for merge_request' do
+ let!(:merge_request) do
+ create(:merge_request,
+ :with_merge_request_pipeline,
+ source_project: project,
+ source_branch: ref,
+ target_project: project,
+ target_branch: 'master')
+ end
+
+ let(:pipeline) { merge_request.merge_request_pipelines.first }
+
+ it 'does not return the pipeline' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.merge_request_event' do
subject { described_class.merge_request_event }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index baad8352185..9d4e18534ae 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -542,7 +542,7 @@ eos
end
end
- describe '#uri_type' do
+ shared_examples '#uri_type' do
it 'returns the URI type at the given path' do
expect(commit.uri_type('files/html')).to be(:tree)
expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
@@ -561,6 +561,20 @@ eos
end
end
+ describe '#uri_type with Gitaly enabled' do
+ it_behaves_like "#uri_type"
+ end
+
+ describe '#uri_type with Rugged enabled', :enable_rugged do
+ it 'calls out to the Rugged implementation' do
+ allow_any_instance_of(Rugged::Tree).to receive(:path).with('files/html').and_call_original
+
+ commit.uri_type('files/html')
+ end
+
+ it_behaves_like '#uri_type'
+ end
+
describe '.from_hash' do
let(:new_commit) { described_class.from_hash(commit.to_hash, project) }
diff --git a/spec/models/concerns/has_ref_spec.rb b/spec/models/concerns/has_ref_spec.rb
index 8aa2fecb18c..6805731fed3 100644
--- a/spec/models/concerns/has_ref_spec.rb
+++ b/spec/models/concerns/has_ref_spec.rb
@@ -18,7 +18,7 @@ describe HasRef do
end
context 'when it was triggered by merge request' do
- let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
let(:pipeline) { merge_request.merge_request_pipelines.first }
let(:build) { create(:ci_build, pipeline: pipeline) }
@@ -67,7 +67,7 @@ describe HasRef do
end
context 'when it is triggered by a merge request' do
- let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
let(:pipeline) { merge_request.merge_request_pipelines.first }
let(:build) { create(:ci_build, tag: false, pipeline: pipeline) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 9dc32a815d8..16624ce47d0 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -810,4 +810,125 @@ describe Group do
it { is_expected.to be_truthy }
end
end
+
+ describe '#first_auto_devops_config' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:group) { create(:group) }
+
+ subject { group.first_auto_devops_config }
+
+ where(:instance_value, :group_value, :config) do
+ # Instance level enabled
+ true | nil | { status: true, scope: :instance }
+ true | true | { status: true, scope: :group }
+ true | false | { status: false, scope: :group }
+
+ # Instance level disabled
+ false | nil | { status: false, scope: :instance }
+ false | true | { status: true, scope: :group }
+ false | false | { status: false, scope: :group }
+ end
+
+ with_them do
+ before do
+ stub_application_setting(auto_devops_enabled: instance_value)
+
+ group.update_attribute(:auto_devops_enabled, group_value)
+ end
+
+ it { is_expected.to eq(config) }
+ end
+
+ context 'with parent groups', :nested_groups do
+ where(:instance_value, :parent_value, :group_value, :config) do
+ # Instance level enabled
+ true | nil | nil | { status: true, scope: :instance }
+ true | nil | true | { status: true, scope: :group }
+ true | nil | false | { status: false, scope: :group }
+
+ true | true | nil | { status: true, scope: :group }
+ true | true | true | { status: true, scope: :group }
+ true | true | false | { status: false, scope: :group }
+
+ true | false | nil | { status: false, scope: :group }
+ true | false | true | { status: true, scope: :group }
+ true | false | false | { status: false, scope: :group }
+
+ # Instance level disable
+ false | nil | nil | { status: false, scope: :instance }
+ false | nil | true | { status: true, scope: :group }
+ false | nil | false | { status: false, scope: :group }
+
+ false | true | nil | { status: true, scope: :group }
+ false | true | true | { status: true, scope: :group }
+ false | true | false | { status: false, scope: :group }
+
+ false | false | nil | { status: false, scope: :group }
+ false | false | true | { status: true, scope: :group }
+ false | false | false | { status: false, scope: :group }
+ end
+
+ with_them do
+ before do
+ stub_application_setting(auto_devops_enabled: instance_value)
+ parent = create(:group, auto_devops_enabled: parent_value)
+
+ group.update!(
+ auto_devops_enabled: group_value,
+ parent: parent
+ )
+ end
+
+ it { is_expected.to eq(config) }
+ end
+ end
+ end
+
+ describe '#auto_devops_enabled?' do
+ subject { group.auto_devops_enabled? }
+
+ context 'when auto devops is explicitly enabled on group' do
+ let(:group) { create(:group, :auto_devops_enabled) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when auto devops is explicitly disabled on group' do
+ let(:group) { create(:group, :auto_devops_disabled) }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when auto devops is implicitly enabled or disabled' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+
+ group.update!(parent: parent_group)
+ end
+
+ context 'when auto devops is enabled on root group' do
+ let(:root_group) { create(:group, :auto_devops_enabled) }
+ let(:subgroup) { create(:group, parent: root_group) }
+ let(:parent_group) { create(:group, parent: subgroup) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when auto devops is disabled on root group' do
+ let(:root_group) { create(:group, :auto_devops_disabled) }
+ let(:subgroup) { create(:group, parent: root_group) }
+ let(:parent_group) { create(:group, parent: subgroup) }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when auto devops is disabled on parent group and enabled on root group' do
+ let(:root_group) { create(:group, :auto_devops_enabled) }
+ let(:parent_group) { create(:group, :auto_devops_disabled, parent: root_group) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 07cb4c9c1e3..42c49e330cc 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -270,6 +270,25 @@ describe MergeRequest do
end
end
+ describe '.recent_target_branches' do
+ let(:project) { create(:project) }
+ let!(:merge_request1) { create(:merge_request, :opened, source_project: project, target_branch: 'feature') }
+ let!(:merge_request2) { create(:merge_request, :closed, source_project: project, target_branch: 'merge-test') }
+ let!(:merge_request3) { create(:merge_request, :opened, source_project: project, target_branch: 'fix') }
+ let!(:merge_request4) { create(:merge_request, :closed, source_project: project, target_branch: 'feature') }
+
+ before do
+ merge_request1.update_columns(updated_at: 1.day.since)
+ merge_request2.update_columns(updated_at: 2.days.since)
+ merge_request3.update_columns(updated_at: 3.days.since)
+ merge_request4.update_columns(updated_at: 4.days.since)
+ end
+
+ it 'returns target branches sort by updated at desc' do
+ expect(described_class.recent_target_branches).to match_array(['feature', 'merge-test', 'fix'])
+ end
+ end
+
describe '#target_branch_sha' do
let(:project) { create(:project, :repository) }
@@ -1331,7 +1350,7 @@ describe MergeRequest do
sha: shas.second)
end
- let!(:merge_request_pipeline) do
+ let!(:detached_merge_request_pipeline) do
create(:ci_pipeline,
source: :merge_request_event,
project: project,
@@ -1357,7 +1376,7 @@ describe MergeRequest do
it 'returns merge request pipeline first' do
expect(merge_request.all_pipelines)
- .to eq([merge_request_pipeline,
+ .to eq([detached_merge_request_pipeline,
branch_pipeline])
end
@@ -1370,7 +1389,7 @@ describe MergeRequest do
sha: shas.first)
end
- let!(:merge_request_pipeline_2) do
+ let!(:detached_merge_request_pipeline_2) do
create(:ci_pipeline,
source: :merge_request_event,
project: project,
@@ -1381,8 +1400,8 @@ describe MergeRequest do
it 'returns merge request pipelines first' do
expect(merge_request.all_pipelines)
- .to eq([merge_request_pipeline_2,
- merge_request_pipeline,
+ .to eq([detached_merge_request_pipeline_2,
+ detached_merge_request_pipeline,
branch_pipeline_2,
branch_pipeline])
end
@@ -1397,7 +1416,7 @@ describe MergeRequest do
sha: shas.first)
end
- let!(:merge_request_pipeline_2) do
+ let!(:detached_merge_request_pipeline_2) do
create(:ci_pipeline,
source: :merge_request_event,
project: project,
@@ -1420,16 +1439,35 @@ describe MergeRequest do
it 'returns only related merge request pipelines' do
expect(merge_request.all_pipelines)
- .to eq([merge_request_pipeline,
+ .to eq([detached_merge_request_pipeline,
branch_pipeline_2,
branch_pipeline])
expect(merge_request_2.all_pipelines)
- .to eq([merge_request_pipeline_2,
+ .to eq([detached_merge_request_pipeline_2,
branch_pipeline_2,
branch_pipeline])
end
end
+
+ context 'when detached merge request pipeline is run on head ref of the merge request' do
+ let!(:detached_merge_request_pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ project: project,
+ ref: merge_request.ref_path,
+ sha: shas.second,
+ merge_request: merge_request)
+ end
+
+ it 'sets the head ref of the merge request to the pipeline ref' do
+ expect(detached_merge_request_pipeline.ref).to match(%r{refs/merge-requests/\d+/head})
+ end
+
+ it 'includes the detached merge request pipeline even though the ref is custom path' do
+ expect(merge_request.all_pipelines).to include(detached_merge_request_pipeline)
+ end
+ end
end
end
@@ -1470,6 +1508,37 @@ describe MergeRequest do
end
end
+ context 'when detached merge request pipeline is run on head ref of the merge request' do
+ let!(:pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ project: merge_request.source_project,
+ ref: merge_request.ref_path,
+ sha: sha,
+ merge_request: merge_request)
+ end
+
+ let(:sha) { merge_request.diff_head_sha }
+
+ it 'sets the head ref of the merge request to the pipeline ref' do
+ expect(pipeline.ref).to match(%r{refs/merge-requests/\d+/head})
+ end
+
+ it 'updates correctly even though the target branch name of the merge request is different from the pipeline ref' do
+ expect { subject }
+ .to change { merge_request.reload.head_pipeline }
+ .from(nil).to(pipeline)
+ end
+
+ context 'when sha is not HEAD of the source branch' do
+ let(:sha) { merge_request.diff_base_sha }
+
+ it 'does not update head pipeline' do
+ expect { subject }.not_to change { merge_request.reload.head_pipeline }
+ end
+ end
+ end
+
context 'when there are no pipelines with the diff head sha' do
it 'does not update the head pipeline' do
expect { subject }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 475fbe56e4d..aadc298ae0b 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -775,4 +775,28 @@ describe Namespace do
end
end
end
+
+ describe '#auto_devops_enabled' do
+ context 'with users' do
+ let(:user) { create(:user) }
+
+ subject { user.namespace.auto_devops_enabled? }
+
+ before do
+ user.namespace.update!(auto_devops_enabled: auto_devops_enabled)
+ end
+
+ context 'when auto devops is explicitly enabled' do
+ let(:auto_devops_enabled) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when auto devops is explicitly disabled' do
+ let(:auto_devops_enabled) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 385b8a7959f..eb6f6ff5faf 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -208,6 +208,24 @@ describe Note do
end
end
+ describe "edited?" do
+ let(:note) { build(:note, updated_by_id: nil, created_at: Time.now, updated_at: Time.now + 5.hours) }
+
+ context "with updated_by" do
+ it "returns true" do
+ note.updated_by = build(:user)
+
+ expect(note.edited?).to be_truthy
+ end
+ end
+
+ context "without updated_by" do
+ it "returns false" do
+ expect(note.edited?).to be_falsy
+ end
+ end
+ end
+
describe "confidential?" do
it "delegates to noteable" do
issue_note = build(:note, :on_issue)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 71bd7972436..5c09faafd83 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -3631,12 +3631,36 @@ describe Project do
subject { project.auto_devops_enabled? }
+ context 'when explicitly enabled' do
+ before do
+ create(:project_auto_devops, project: project)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when explicitly disabled' do
+ before do
+ create(:project_auto_devops, project: project, enabled: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
context 'when enabled in settings' do
before do
stub_application_setting(auto_devops_enabled: true)
end
it { is_expected.to be_truthy }
+ end
+
+ context 'when disabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it { is_expected.to be_falsey }
context 'when explicitly enabled' do
before do
@@ -3648,34 +3672,91 @@ describe Project do
context 'when explicitly disabled' do
before do
- create(:project_auto_devops, project: project, enabled: false)
+ create(:project_auto_devops, :disabled, project: project)
end
it { is_expected.to be_falsey }
end
end
- context 'when disabled in settings' do
+ context 'when force_autodevops_on_by_default is enabled for the project' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with group parents' do
+ let(:instance_enabled) { true }
+
before do
- stub_application_setting(auto_devops_enabled: false)
+ stub_application_setting(auto_devops_enabled: instance_enabled)
+ project.update!(namespace: parent_group)
end
- it { is_expected.to be_falsey }
+ context 'when enabled on parent' do
+ let(:parent_group) { create(:group, :auto_devops_enabled) }
- context 'when explicitly enabled' do
- before do
- create(:project_auto_devops, project: project)
+ context 'when auto devops instance enabled' do
+ it { is_expected.to be_truthy }
end
- it { is_expected.to be_truthy }
+ context 'when auto devops instance disabled' do
+ let(:instance_disabled) { false }
+
+ it { is_expected.to be_truthy }
+ end
end
- context 'when force_autodevops_on_by_default is enabled for the project' do
- before do
- Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100)
+ context 'when disabled on parent' do
+ let(:parent_group) { create(:group, :auto_devops_disabled) }
+
+ context 'when auto devops instance enabled' do
+ it { is_expected.to be_falsy }
end
- it { is_expected.to be_truthy }
+ context 'when auto devops instance disabled' do
+ let(:instance_disabled) { false }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'when enabled on root parent', :nested_groups do
+ let(:parent_group) { create(:group, parent: create(:group, :auto_devops_enabled)) }
+
+ context 'when auto devops instance enabled' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when auto devops instance disabled' do
+ let(:instance_disabled) { false }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when explicitly disabled on parent' do
+ let(:parent_group) { create(:group, :auto_devops_disabled, parent: create(:group, :auto_devops_enabled)) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'when disabled on root parent', :nested_groups do
+ let(:parent_group) { create(:group, parent: create(:group, :auto_devops_disabled)) }
+
+ context 'when auto devops instance enabled' do
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when auto devops instance disabled' do
+ let(:instance_disabled) { false }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when explicitly disabled on parent' do
+ let(:parent_group) { create(:group, :auto_devops_disabled, parent: create(:group, :auto_devops_enabled)) }
+
+ it { is_expected.to be_falsy }
+ end
end
end
end
@@ -3722,15 +3803,52 @@ describe Project do
end
end
end
+
+ context 'when enabled on group' do
+ it 'has auto devops implicitly enabled' do
+ project.update(namespace: create(:group, :auto_devops_enabled))
+
+ expect(project).to have_auto_devops_implicitly_enabled
+ end
+ end
+
+ context 'when enabled on parent group' do
+ it 'has auto devops implicitly enabled' do
+ subgroup = create(:group, parent: create(:group, :auto_devops_enabled))
+ project.update(namespace: subgroup)
+
+ expect(project).to have_auto_devops_implicitly_enabled
+ end
+ end
end
describe '#has_auto_devops_implicitly_disabled?' do
+ set(:project) { create(:project) }
+
before do
allow(Feature).to receive(:enabled?).and_call_original
Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0)
end
- set(:project) { create(:project) }
+ context 'when explicitly disabled' do
+ before do
+ create(:project_auto_devops, project: project, enabled: false)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
+
+ context 'when explicitly enabled' do
+ before do
+ create(:project_auto_devops, project: project, enabled: true)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
context 'when enabled in settings' do
before do
@@ -3753,6 +3871,8 @@ describe Project do
context 'when force_autodevops_on_by_default is enabled for the project' do
before do
+ create(:project_auto_devops, project: project, enabled: false)
+
Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100)
end
@@ -3761,23 +3881,20 @@ describe Project do
end
end
- context 'when explicitly disabled' do
- before do
- create(:project_auto_devops, project: project, enabled: false)
- end
+ context 'when disabled on group' do
+ it 'has auto devops implicitly disabled' do
+ project.update!(namespace: create(:group, :auto_devops_disabled))
- it 'does not have auto devops implicitly disabled' do
- expect(project).not_to have_auto_devops_implicitly_disabled
+ expect(project).to have_auto_devops_implicitly_disabled
end
end
- context 'when explicitly enabled' do
- before do
- create(:project_auto_devops, project: project, enabled: true)
- end
+ context 'when disabled on parent group' do
+ it 'has auto devops implicitly disabled' do
+ subgroup = create(:group, parent: create(:group, :auto_devops_disabled))
+ project.update!(namespace: subgroup)
- it 'does not have auto devops implicitly disabled' do
- expect(project).not_to have_auto_devops_implicitly_disabled
+ expect(project).to have_auto_devops_implicitly_disabled
end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 70630467d24..6599b4e765a 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1095,65 +1095,69 @@ describe Repository do
end
end
- describe '#exists?' do
- it 'returns true when a repository exists' do
- expect(repository.exists?).to be(true)
- end
-
- it 'returns false if no full path can be constructed' do
- allow(repository).to receive(:full_path).and_return(nil)
-
- expect(repository.exists?).to be(false)
- end
-
- context 'with broken storage', :broken_storage do
- it 'should raise a storage error' do
- expect_to_raise_storage_error { broken_repository.exists? }
- end
- end
-
+ shared_examples 'asymmetric cached method' do |method|
context 'asymmetric caching', :use_clean_rails_memory_store_caching, :request_store do
let(:cache) { repository.send(:cache) }
let(:request_store_cache) { repository.send(:request_store_cache) }
context 'when it returns true' do
before do
- expect(repository.raw_repository).to receive(:exists?).once.and_return(true)
+ expect(repository.raw_repository).to receive(method).once.and_return(true)
end
it 'caches the output in RequestStore' do
expect do
- repository.exists?
- end.to change { request_store_cache.read(:exists?) }.from(nil).to(true)
+ repository.send(method)
+ end.to change { request_store_cache.read(method) }.from(nil).to(true)
end
it 'caches the output in RepositoryCache' do
expect do
- repository.exists?
- end.to change { cache.read(:exists?) }.from(nil).to(true)
+ repository.send(method)
+ end.to change { cache.read(method) }.from(nil).to(true)
end
end
context 'when it returns false' do
before do
- expect(repository.raw_repository).to receive(:exists?).once.and_return(false)
+ expect(repository.raw_repository).to receive(method).once.and_return(false)
end
it 'caches the output in RequestStore' do
expect do
- repository.exists?
- end.to change { request_store_cache.read(:exists?) }.from(nil).to(false)
+ repository.send(method)
+ end.to change { request_store_cache.read(method) }.from(nil).to(false)
end
it 'does NOT cache the output in RepositoryCache' do
expect do
- repository.exists?
- end.not_to change { cache.read(:exists?) }.from(nil)
+ repository.send(method)
+ end.not_to change { cache.read(method) }.from(nil)
end
end
end
end
+ describe '#exists?' do
+ it 'returns true when a repository exists' do
+ expect(repository.exists?).to be(true)
+ end
+
+ it 'returns false if no full path can be constructed' do
+ allow(repository).to receive(:full_path).and_return(nil)
+
+ expect(repository.exists?).to be(false)
+ end
+
+ context 'with broken storage', :broken_storage do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error { broken_repository.exists? }
+ end
+ end
+
+ it_behaves_like 'asymmetric cached method', :exists?
+ end
+
describe '#has_visible_content?' do
before do
# If raw_repository.has_visible_content? gets called more than once then
@@ -1271,6 +1275,8 @@ describe Repository do
repository.root_ref
repository.root_ref
end
+
+ it_behaves_like 'asymmetric cached method', :root_ref
end
describe '#expire_root_ref_cache' do
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 0ad50c6f91f..92bdaa8b8b8 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -76,15 +76,10 @@ describe GroupPolicy do
context 'with no user and public project' do
let(:project) { create(:project, :public) }
- let(:user) { create(:user) }
let(:current_user) { nil }
before do
- Projects::GroupLinks::CreateService.new(
- project,
- user,
- link_group_access: ProjectGroupLink::DEVELOPER
- ).execute(group)
+ create(:project_group_link, project: project, group: group)
end
it { expect_disallowed(:read_group) }
@@ -96,11 +91,7 @@ describe GroupPolicy do
let(:current_user) { create(:user) }
before do
- Projects::GroupLinks::CreateService.new(
- project,
- user,
- link_group_access: ProjectGroupLink::DEVELOPER
- ).execute(group)
+ create(:project_group_link, project: project, group: group)
end
it { expect_disallowed(:read_group) }
diff --git a/spec/rack_servers/puma_spec.rb b/spec/rack_servers/puma_spec.rb
index 431fab87857..891df4f1a66 100644
--- a/spec/rack_servers/puma_spec.rb
+++ b/spec/rack_servers/puma_spec.rb
@@ -44,11 +44,9 @@ describe 'Puma' do
end
after(:all) do
- begin
- WebMock.disable_net_connect!(allow_localhost: true)
- Process.kill('TERM', @puma_master_pid)
- rescue Errno::ESRCH
- end
+ WebMock.disable_net_connect!(allow_localhost: true)
+ Process.kill('TERM', @puma_master_pid)
+ rescue Errno::ESRCH
end
def wait_puma_boot!(master_pid, ready_file)
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index cd85151ec1b..b184c92824a 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -26,6 +26,21 @@ describe API::Internal do
expect(json_response['redis']).to be(false)
end
+
+ context 'authenticating' do
+ it 'authenticates using a header' do
+ get api("/internal/check"),
+ headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'returns 401 when no credentials provided' do
+ get(api("/internal/check"))
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
end
describe 'GET /internal/broadcast_message' do
@@ -237,6 +252,14 @@ describe API::Internal do
expect(json_response['name']).to eq(user.name)
end
+
+ it 'responds successfully when a user is not found' do
+ get(api("/internal/discover"), params: { username: 'noone', secret_token: secret_token })
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(response.body).to eq('null')
+ end
end
describe "GET /internal/authorized_keys" do
@@ -324,7 +347,6 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_project_path"]).to eq(project.wiki.full_path)
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user.reload.last_activity_on).to be_nil
@@ -337,7 +359,6 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_project_path"]).to eq(project.wiki.full_path)
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user.reload.last_activity_on).to eql(Date.today)
@@ -350,7 +371,6 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gl_project_path"]).to eq(project.full_path)
expect(json_response["gitaly"]).not_to be_nil
@@ -370,7 +390,6 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gl_project_path"]).to eq(project.full_path)
expect(json_response["gitaly"]).not_to be_nil
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index fee6312a9c7..4259fda7f04 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -4,32 +4,406 @@ describe API::MergeRequests do
include ProjectForksHelper
let(:base_time) { Time.now }
- let(:user) { create(:user) }
- let(:admin) { create(:user, :admin) }
- let(:non_member) { create(:user) }
- let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
+ set(:user) { create(:user) }
+ set(:admin) { create(:user, :admin) }
+ let(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
- let(:pipeline) { create(:ci_empty_pipeline) }
- let(:milestone1) { create(:milestone, title: '0.9', project: project) }
+ let(:milestone1) { create(:milestone, title: '0.9', project: project) }
let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
- let!(:label) do
- create(:label, title: 'label', color: '#FFAABB', project: project)
- end
- let!(:label2) { create(:label, title: 'a-test', color: '#FFFFFF', project: project) }
- let!(:label_link) { create(:label_link, label: label, target: merge_request) }
- let!(:label_link2) { create(:label_link, label: label2, target: merge_request) }
- let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request) }
- let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) }
+ let(:label) { create(:label, title: 'label', color: '#FFAABB', project: project) }
+ let(:label2) { create(:label, title: 'a-test', color: '#FFFFFF', project: project) }
before do
project.add_reporter(user)
end
+ shared_context 'with labels' do
+ before do
+ create(:label_link, label: label, target: merge_request)
+ create(:label_link, label: label2, target: merge_request)
+ end
+ end
+
+ shared_examples 'merge requests list' do
+ context 'when unauthenticated' do
+ it 'returns merge requests for public projects' do
+ get api(endpoint_path)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'when authenticated' do
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new do
+ get api(endpoint_path, user)
+ end
+
+ create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+
+ merge_request = create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+
+ merge_request.metrics.update!(merged_by: user,
+ latest_closed_by: user,
+ latest_closed_at: 1.hour.ago,
+ merged_at: 2.hours.ago)
+
+ expect do
+ get api(endpoint_path, user)
+ end.not_to exceed_query_limit(control)
+ end
+
+ context 'with labels' do
+ include_context 'with labels'
+
+ it 'returns an array of all merge_requests' do
+ get api(endpoint_path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
+ expect(json_response.last['merge_commit_sha']).to be_nil
+ expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
+ expect(json_response.last['downvotes']).to eq(0)
+ expect(json_response.last['upvotes']).to eq(0)
+ expect(json_response.last['labels']).to eq([label2.title, label.title])
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
+ expect(json_response.first['merge_commit_sha']).not_to be_nil
+ expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
+ end
+ end
+
+ it 'returns an array of all merge_requests using simple mode' do
+ path = endpoint_path + '?view=simple'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ expect(json_response.last['iid']).to eq(merge_request.iid)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.first['iid']).to eq(merge_request_merged.iid)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first).to have_key('web_url')
+ end
+
+ it 'returns an array of all merge_requests' do
+ path = endpoint_path + '?state'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it 'returns an array of open merge_requests' do
+ path = endpoint_path + '?state=opened'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it 'returns an array of closed merge_requests' do
+ path = endpoint_path + '?state=closed'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(merge_request_closed.title)
+ end
+
+ it 'returns an array of merged merge_requests' do
+ path = endpoint_path + '?state=merged'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ end
+
+ it 'matches V4 response schema' do
+ get api(endpoint_path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/merge_requests')
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get api(endpoint_path, user), params: { milestone: '1.0.0' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api(endpoint_path, user), params: { milestone: 'foo' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of merge requests in given milestone' do
+ get api(endpoint_path, user), params: { milestone: '0.9' }
+
+ closed_issues = json_response.select { |mr| mr['id'] == merge_request_closed.id }
+ expect(closed_issues.length).to eq(1)
+ expect(closed_issues.first['title']).to eq merge_request_closed.title
+ end
+
+ it 'returns an array of merge requests matching state in milestone' do
+ get api(endpoint_path, user), params: { milestone: '0.9', state: 'closed' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request_closed.id)
+ end
+
+ context 'with labels' do
+ include_context 'with labels'
+
+ it 'returns an array of labeled merge requests' do
+ path = endpoint_path + "?labels=#{label.title}"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label2.title, label.title])
+ end
+
+ it 'returns an array of labeled merge requests where all labels match' do
+ path = endpoint_path + "?labels=#{label.title},foo,bar"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no merge request matches labels' do
+ path = endpoint_path + '?labels=foo,bar'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of labeled merge requests where all labels match' do
+ path = endpoint_path + "?labels[]=#{label.title}&labels[]=#{label2.title}"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label2.title, label.title])
+ end
+
+ it 'returns an array of merge requests with any label when filtering by any label' do
+ get api(endpoint_path, user), params: { labels: [" #{label.title} ", " #{label2.title} "] }
+
+ expect_paginated_array_response
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label2.title, label.title])
+ expect(json_response.first['id']).to eq(merge_request.id)
+ end
+
+ it 'returns an array of merge requests with any label when filtering by any label' do
+ get api(endpoint_path, user), params: { labels: ["#{label.title} , #{label2.title}"] }
+
+ expect_paginated_array_response
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label2.title, label.title])
+ expect(json_response.first['id']).to eq(merge_request.id)
+ end
+
+ it 'returns an array of merge requests with any label when filtering by any label' do
+ get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_ANY }
+
+ expect_paginated_array_response
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request.id)
+ end
+
+ it 'returns an array of merge requests without a label when filtering by no label' do
+ get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_NONE }
+
+ response_ids = json_response.map { |merge_request| merge_request['id'] }
+
+ expect_paginated_array_response
+ expect(response_ids).to contain_exactly(merge_request_closed.id, merge_request_merged.id, merge_request_locked.id)
+ end
+ end
+
+ it 'returns an array of labeled merge requests that are merged for a milestone' do
+ bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project)
+
+ mr1 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone)
+ mr2 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1)
+ mr3 = create(:merge_request, state: 'closed', source_project: project, target_project: project, milestone: milestone1)
+ _mr = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1)
+
+ create(:label_link, label: bug_label, target: mr1)
+ create(:label_link, label: bug_label, target: mr2)
+ create(:label_link, label: bug_label, target: mr3)
+
+ path = endpoint_path + "?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(mr2.id)
+ end
+
+ context 'with ordering' do
+ before do
+ @mr_later = mr_with_later_created_and_updated_at_time
+ @mr_earlier = mr_with_earlier_created_and_updated_at_time
+ end
+
+ it 'returns an array of merge_requests in ascending order' do
+ path = endpoint_path + '?sort=asc'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'returns an array of merge_requests in descending order' do
+ path = endpoint_path + '?sort=desc'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ context '2 merge requests with equal created_at' do
+ let!(:closed_mr2) do
+ create :merge_request,
+ state: 'closed',
+ milestone: milestone1,
+ author: user,
+ assignee: user,
+ source_project: project,
+ target_project: project,
+ title: "Test",
+ created_at: @mr_earlier.created_at
+ end
+
+ it 'page breaks first page correctly' do
+ get api("#{endpoint_path}?sort=desc&per_page=4", user)
+
+ response_ids = json_response.map { |merge_request| merge_request['id'] }
+
+ expect(response_ids).to include(closed_mr2.id)
+ expect(response_ids).not_to include(@mr_earlier.id)
+ end
+
+ it 'page breaks second page correctly' do
+ get api("#{endpoint_path}?sort=desc&per_page=4&page=2", user)
+
+ response_ids = json_response.map { |merge_request| merge_request['id'] }
+
+ expect(response_ids).not_to include(closed_mr2.id)
+ expect(response_ids).to include(@mr_earlier.id)
+ end
+ end
+
+ it 'returns an array of merge_requests ordered by updated_at' do
+ path = endpoint_path + '?order_by=updated_at'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'returns an array of merge_requests ordered by created_at' do
+ path = endpoint_path + '?order_by=created_at&sort=asc'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ context 'source_branch param' do
+ it 'returns merge requests with the given source branch' do
+ get api(endpoint_path, user), params: { source_branch: merge_request_closed.source_branch, state: 'all' }
+
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
+ end
+ end
+
+ context 'target_branch param' do
+ it 'returns merge requests with the given target branch' do
+ get api(endpoint_path, user), params: { target_branch: merge_request_closed.target_branch, state: 'all' }
+
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
+ end
+ end
+ end
+ end
+
describe 'route shadowing' do
include GrapePathHelpers::NamedRouteMatcher
@@ -356,6 +730,9 @@ describe API::MergeRequests do
describe "GET /projects/:id/merge_requests/:merge_request_iid" do
it 'exposes known attributes' do
+ create(:award_emoji, :downvote, awardable: merge_request)
+ create(:award_emoji, :upvote, awardable: merge_request)
+
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(response).to have_gitlab_http_status(200)
@@ -405,6 +782,8 @@ describe API::MergeRequests do
end
context 'merge_request_metrics' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
before do
merge_request.metrics.update!(merged_by: user,
latest_closed_by: user,
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index 71788028cbf..53271550e8b 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -17,6 +17,10 @@ describe "Groups", "routing" do
expect(get("/#{group_path}")).to route_to('groups#show', id: group_path)
end
+ it "to #details" do
+ expect(get("/groups/#{group_path}/-/details")).to route_to('groups#details', id: group_path)
+ end
+
it "to #activity" do
expect(get("/groups/#{group_path}/-/activity")).to route_to('groups#activity', id: group_path)
end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 11040862129..c8308a0ae85 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -3,13 +3,16 @@ require 'spec_helper'
describe PipelineEntity do
include Gitlab::Routing
+ set(:project) { create(:project) }
set(:user) { create(:user) }
+ set(:project) { create(:project) }
let(:request) { double('request') }
before do
stub_not_protect_default_branch
allow(request).to receive(:current_user).and_return(user)
+ allow(request).to receive(:project).and_return(project)
end
let(:entity) do
@@ -132,12 +135,12 @@ describe PipelineEntity do
end
context 'when pipeline is detached merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
let(:project) { merge_request.target_project }
let(:pipeline) { merge_request.merge_request_pipelines.first }
it 'makes detached flag true' do
- expect(subject[:flags][:detached]).to be_truthy
+ expect(subject[:flags][:detached_merge_request_pipeline]).to be_truthy
end
context 'when user is a developer' do
@@ -173,5 +176,19 @@ describe PipelineEntity do
end
end
end
+
+ context 'when pipeline is merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_merge_request_pipeline, merge_sha: 'abc') }
+ let(:project) { merge_request.target_project }
+ let(:pipeline) { merge_request.merge_request_pipelines.first }
+
+ it 'makes detached flag false' do
+ expect(subject[:flags][:detached_merge_request_pipeline]).to be_falsy
+ end
+
+ it 'makes atached flag true' do
+ expect(subject[:flags][:merge_request_pipeline]).to be_truthy
+ end
+ end
end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index a21487938a0..0fdd675aa01 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -5,7 +5,7 @@ describe PipelineSerializer do
set(:user) { create(:user) }
let(:serializer) do
- described_class.new(current_user: user)
+ described_class.new(current_user: user, project: project)
end
before do
@@ -102,20 +102,20 @@ describe PipelineSerializer do
let!(:merge_request_1) do
create(:merge_request,
- :with_merge_request_pipeline,
+ :with_detached_merge_request_pipeline,
target_project: project,
target_branch: 'master',
source_project: project,
- source_branch: 'feature-1')
+ source_branch: 'feature')
end
let!(:merge_request_2) do
create(:merge_request,
- :with_merge_request_pipeline,
+ :with_detached_merge_request_pipeline,
target_project: project,
target_branch: 'master',
source_project: project,
- source_branch: 'feature-2')
+ source_branch: '2-mb-file')
end
before do
diff --git a/spec/services/groups/auto_devops_service_spec.rb b/spec/services/groups/auto_devops_service_spec.rb
new file mode 100644
index 00000000000..7f8ab517cef
--- /dev/null
+++ b/spec/services/groups/auto_devops_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Groups::AutoDevopsService, '#execute' do
+ set(:group) { create(:group) }
+ set(:user) { create(:user) }
+ let(:group_params) { { auto_devops_enabled: '0' } }
+ let(:service) { described_class.new(group, user, group_params) }
+
+ context 'when user does not have enough privileges' do
+ it 'raises exception' do
+ group.add_developer(user)
+
+ expect do
+ service.execute
+ end.to raise_exception(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'when user has enough privileges' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'updates group auto devops enabled accordingly' do
+ service.execute
+
+ expect(group.auto_devops_enabled).to eq(false)
+ end
+
+ context 'when group has projects' do
+ it 'reflects changes on projects' do
+ project_1 = create(:project, namespace: group)
+
+ service.execute
+
+ expect(project_1).not_to have_auto_devops_implicitly_enabled
+ end
+ end
+
+ context 'when group has subgroups' do
+ it 'reflects changes on subgroups' do
+ subgroup_1 = create(:group, parent: group)
+
+ service.execute
+
+ expect(subgroup_1.auto_devops_enabled?).to eq(false)
+ end
+
+ context 'when subgroups have projects', :nested_groups do
+ it 'reflects changes on projects' do
+ subgroup_1 = create(:group, parent: group)
+ project_1 = create(:project, namespace: subgroup_1)
+
+ service.execute
+
+ expect(project_1).not_to have_auto_devops_implicitly_enabled
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index dfbdfa2ab69..d3a8ee46f85 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -128,10 +128,8 @@ describe Projects::DestroyService do
it 'keeps project team intact upon an error' do
perform_enqueued_jobs do
- begin
- destroy_project(project, user, {})
- rescue ::Redis::CannotConnectError
- end
+ destroy_project(project, user, {})
+ rescue ::Redis::CannotConnectError
end
expect(project.team.members.count).to eq 2
diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb
index f7d74df0656..4cf34d43117 100644
--- a/spec/support/api/schema_matcher.rb
+++ b/spec/support/api/schema_matcher.rb
@@ -1,10 +1,16 @@
module SchemaPath
- def self.expand(schema, dir = '')
- Rails.root.join(dir, 'spec', "fixtures/api/schemas/#{schema}.json").to_s
+ def self.expand(schema, dir = nil)
+ if Gitlab.ee? && dir.nil?
+ ee_path = expand(schema, 'ee')
+
+ return ee_path if File.exist?(ee_path)
+ end
+
+ Rails.root.join(dir.to_s, 'spec', "fixtures/api/schemas/#{schema}.json").to_s
end
end
-RSpec::Matchers.define :match_response_schema do |schema, dir: '', **options|
+RSpec::Matchers.define :match_response_schema do |schema, dir: nil, **options|
match do |response|
@errors = JSON::Validator.fully_validate(
SchemaPath.expand(schema, dir), response.body, options)
@@ -18,8 +24,16 @@ RSpec::Matchers.define :match_response_schema do |schema, dir: '', **options|
end
end
-RSpec::Matchers.define :match_schema do |schema, dir: '', **options|
+RSpec::Matchers.define :match_schema do |schema, dir: nil, **options|
match do |data|
- JSON::Validator.validate!(SchemaPath.expand(schema, dir), data, options)
+ @errors = JSON::Validator.fully_validate(
+ SchemaPath.expand(schema, dir), data, options)
+
+ @errors.empty?
+ end
+
+ failure_message do |response|
+ "didn't match the schema defined by #{SchemaPath.expand(schema, dir)}" \
+ " The validation errors were:\n#{@errors.join("\n")}"
end
end
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index e883d33f671..15037222630 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -3,6 +3,8 @@ shared_examples 'an unauthorized API user' do
end
shared_examples 'time tracking endpoints' do |issuable_name|
+ let(:non_member) { create(:user) }
+
issuable_collection_name = issuable_name.pluralize
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 6cdc19ac2e5..ca28325eab9 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -18,12 +18,10 @@ module GraphqlHelpers
# Runs a block inside a BatchLoader::Executor wrapper
def batch(max_queries: nil, &blk)
wrapper = proc do
- begin
- BatchLoader::Executor.ensure_current
- yield
- ensure
- BatchLoader::Executor.clear_current
- end
+ BatchLoader::Executor.ensure_current
+ yield
+ ensure
+ BatchLoader::Executor.clear_current
end
if max_queries
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index e0c50e533a6..30c8477f16a 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -23,15 +23,13 @@ module StubObjectStorage
Fog.mock!
::Fog::Storage.new(connection_params).tap do |connection|
- begin
- connection.directories.create(key: remote_directory)
+ connection.directories.create(key: remote_directory)
- # Cleanup remaining files
- connection.directories.each do |directory|
- directory.files.map(&:destroy)
- end
- rescue Excon::Error::Conflict
+ # Cleanup remaining files
+ connection.directories.each do |directory|
+ directory.files.map(&:destroy)
end
+ rescue Excon::Error::Conflict
end
end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 80a22134021..dc902d373b8 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -202,12 +202,10 @@ module TestEnv
socket = Gitlab::GitalyClient.address('default').sub('unix:', '')
Integer(sleep_time / sleep_interval).times do
- begin
- Socket.unix(socket)
- return
- rescue
- sleep sleep_interval
- end
+ Socket.unix(socket)
+ return
+ rescue
+ sleep sleep_interval
end
raise "could not connect to gitaly at #{socket.inspect} after #{sleep_time} seconds"
diff --git a/spec/support/pg_stat_activity.rb b/spec/support/pg_stat_activity.rb
deleted file mode 100644
index f93fba08a19..00000000000
--- a/spec/support/pg_stat_activity.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.configure do |config|
- config.before do
- if Gitlab::Database.postgresql? && ENV['PG_STAT_WARNING_THRESHOLD']
- warning_threshold = ENV['PG_STAT_WARNING_THRESHOLD'].to_i
- results = ActiveRecord::Base.connection.execute('SELECT * FROM pg_stat_activity')
- ntuples = results.ntuples
-
- warn("pg_stat_activity count: #{ntuples}")
-
- if ntuples > warning_threshold
- results.each do |result|
- warn result.inspect
- end
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb
deleted file mode 100644
index 32e3b81c3c5..00000000000
--- a/spec/support/shared_examples/requests/api/merge_requests_list.rb
+++ /dev/null
@@ -1,366 +0,0 @@
-shared_examples 'merge requests list' do
- context 'when unauthenticated' do
- it 'returns merge requests for public projects' do
- get api(endpoint_path)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- end
- end
-
- context 'when authenticated' do
- it 'avoids N+1 queries' do
- control = ActiveRecord::QueryRecorder.new do
- get api(endpoint_path, user)
- end
-
- create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
-
- merge_request = create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
-
- merge_request.metrics.update!(merged_by: user,
- latest_closed_by: user,
- latest_closed_at: 1.hour.ago,
- merged_at: 2.hours.ago)
-
- expect do
- get api(endpoint_path, user)
- end.not_to exceed_query_limit(control)
- end
-
- it 'returns an array of all merge_requests' do
- get api(endpoint_path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- expect(json_response.last['title']).to eq(merge_request.title)
- expect(json_response.last).to have_key('web_url')
- expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
- expect(json_response.last['merge_commit_sha']).to be_nil
- expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
- expect(json_response.last['downvotes']).to eq(1)
- expect(json_response.last['upvotes']).to eq(1)
- expect(json_response.last['labels']).to eq([label2.title, label.title])
- expect(json_response.first['title']).to eq(merge_request_merged.title)
- expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
- expect(json_response.first['merge_commit_sha']).not_to be_nil
- expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
- end
-
- it 'returns an array of all merge_requests using simple mode' do
- path = endpoint_path + '?view=simple'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- expect(json_response.last['iid']).to eq(merge_request.iid)
- expect(json_response.last['title']).to eq(merge_request.title)
- expect(json_response.last).to have_key('web_url')
- expect(json_response.first['iid']).to eq(merge_request_merged.iid)
- expect(json_response.first['title']).to eq(merge_request_merged.title)
- expect(json_response.first).to have_key('web_url')
- end
-
- it 'returns an array of all merge_requests' do
- path = endpoint_path + '?state'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- expect(json_response.last['title']).to eq(merge_request.title)
- end
-
- it 'returns an array of open merge_requests' do
- path = endpoint_path + '?state=opened'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.last['title']).to eq(merge_request.title)
- end
-
- it 'returns an array of closed merge_requests' do
- path = endpoint_path + '?state=closed'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['title']).to eq(merge_request_closed.title)
- end
-
- it 'returns an array of merged merge_requests' do
- path = endpoint_path + '?state=merged'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['title']).to eq(merge_request_merged.title)
- end
-
- it 'matches V4 response schema' do
- get api(endpoint_path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/merge_requests')
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get api(endpoint_path, user), params: { milestone: '1.0.0' }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if milestone does not exist' do
- get api(endpoint_path, user), params: { milestone: 'foo' }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an array of merge requests in given milestone' do
- get api(endpoint_path, user), params: { milestone: '0.9' }
-
- closed_issues = json_response.select { |mr| mr['id'] == merge_request_closed.id }
- expect(closed_issues.length).to eq(1)
- expect(closed_issues.first['title']).to eq merge_request_closed.title
- end
-
- it 'returns an array of merge requests matching state in milestone' do
- get api(endpoint_path, user), params: { milestone: '0.9', state: 'closed' }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request_closed.id)
- end
-
- it 'returns an array of labeled merge requests' do
- path = endpoint_path + "?labels=#{label.title}"
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['labels']).to eq([label2.title, label.title])
- end
-
- it 'returns an array of labeled merge requests where all labels match' do
- path = endpoint_path + "?labels=#{label.title},foo,bar"
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if no merge request matches labels' do
- path = endpoint_path + '?labels=foo,bar'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an array of labeled merge requests where all labels match' do
- path = endpoint_path + "?labels[]=#{label.title}&labels[]=#{label2.title}"
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['labels']).to eq([label2.title, label.title])
- end
-
- it 'returns an array of merge requests with any label when filtering by any label' do
- get api(endpoint_path, user), params: { labels: [" #{label.title} ", " #{label2.title} "] }
-
- expect_paginated_array_response
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['labels']).to eq([label2.title, label.title])
- expect(json_response.first['id']).to eq(merge_request.id)
- end
-
- it 'returns an array of merge requests with any label when filtering by any label' do
- get api(endpoint_path, user), params: { labels: ["#{label.title} , #{label2.title}"] }
-
- expect_paginated_array_response
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['labels']).to eq([label2.title, label.title])
- expect(json_response.first['id']).to eq(merge_request.id)
- end
-
- it 'returns an array of merge requests with any label when filtering by any label' do
- get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_ANY }
-
- expect_paginated_array_response
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request.id)
- end
-
- it 'returns an array of merge requests without a label when filtering by no label' do
- get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_NONE }
-
- response_ids = json_response.map { |merge_request| merge_request['id'] }
-
- expect_paginated_array_response
- expect(response_ids).to contain_exactly(merge_request_closed.id, merge_request_merged.id, merge_request_locked.id)
- end
-
- it 'returns an array of labeled merge requests that are merged for a milestone' do
- bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project)
-
- mr1 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone)
- mr2 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1)
- mr3 = create(:merge_request, state: 'closed', source_project: project, target_project: project, milestone: milestone1)
- _mr = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1)
-
- create(:label_link, label: bug_label, target: mr1)
- create(:label_link, label: bug_label, target: mr2)
- create(:label_link, label: bug_label, target: mr3)
-
- path = endpoint_path + "?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged"
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(mr2.id)
- end
-
- context 'with ordering' do
- before do
- @mr_later = mr_with_later_created_and_updated_at_time
- @mr_earlier = mr_with_earlier_created_and_updated_at_time
- end
-
- it 'returns an array of merge_requests in ascending order' do
- path = endpoint_path + '?sort=asc'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort)
- end
-
- it 'returns an array of merge_requests in descending order' do
- path = endpoint_path + '?sort=desc'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort.reverse)
- end
-
- context '2 merge requests with equal created_at' do
- let!(:closed_mr2) do
- create :merge_request,
- state: 'closed',
- milestone: milestone1,
- author: user,
- assignee: user,
- source_project: project,
- target_project: project,
- title: "Test",
- created_at: @mr_earlier.created_at
- end
-
- it 'page breaks first page correctly' do
- get api("#{endpoint_path}?sort=desc&per_page=4", user)
-
- response_ids = json_response.map { |merge_request| merge_request['id'] }
-
- expect(response_ids).to include(closed_mr2.id)
- expect(response_ids).not_to include(@mr_earlier.id)
- end
-
- it 'page breaks second page correctly' do
- get api("#{endpoint_path}?sort=desc&per_page=4&page=2", user)
-
- response_ids = json_response.map { |merge_request| merge_request['id'] }
-
- expect(response_ids).not_to include(closed_mr2.id)
- expect(response_ids).to include(@mr_earlier.id)
- end
- end
-
- it 'returns an array of merge_requests ordered by updated_at' do
- path = endpoint_path + '?order_by=updated_at'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
- expect(response_dates).to eq(response_dates.sort.reverse)
- end
-
- it 'returns an array of merge_requests ordered by created_at' do
- path = endpoint_path + '?order_by=created_at&sort=asc'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort)
- end
- end
-
- context 'source_branch param' do
- it 'returns merge requests with the given source branch' do
- get api(endpoint_path, user), params: { source_branch: merge_request_closed.source_branch, state: 'all' }
-
- expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
- end
- end
-
- context 'target_branch param' do
- it 'returns merge requests with the given target branch' do
- get api(endpoint_path, user), params: { target_branch: merge_request_closed.target_branch, state: 'all' }
-
- expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
- end
- end
- end
-end
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
index 6b50670c3c0..736809eee5b 100644
--- a/spec/tasks/gitlab/storage_rake_spec.rb
+++ b/spec/tasks/gitlab/storage_rake_spec.rb
@@ -1,6 +1,6 @@
require 'rake_helper'
-describe 'rake gitlab:storage:*' do
+describe 'rake gitlab:storage:*', :sidekiq do
before do
Rake.application.rake_require 'tasks/gitlab/storage'
@@ -43,9 +43,7 @@ describe 'rake gitlab:storage:*' do
end
end
- describe 'gitlab:storage:migrate_to_hashed' do
- let(:task) { 'gitlab:storage:migrate_to_hashed' }
-
+ shared_examples "make sure database is writable" do
context 'read-only database' do
it 'does nothing' do
expect(Gitlab::Database).to receive(:read_only?).and_return(true)
@@ -55,48 +53,68 @@ describe 'rake gitlab:storage:*' do
expect { run_rake_task(task) }.to output(/This task requires database write access. Exiting./).to_stderr
end
end
+ end
- context '0 legacy projects' do
- it 'does nothing' do
- expect(::HashedStorage::MigratorWorker).not_to receive(:perform_async)
+ shared_examples "handles custom BATCH env var" do |worker_klass|
+ context 'in batches of 1' do
+ before do
+ stub_env('BATCH' => 1)
+ end
+
+ it "enqueues one #{worker_klass} per project" do
+ projects.each do |project|
+ expect(worker_klass).to receive(:perform_async).with(project.id, project.id)
+ end
run_rake_task(task)
end
end
- context '3 legacy projects' do
- let(:projects) { create_list(:project, 3, :legacy_storage) }
+ context 'in batches of 2' do
+ before do
+ stub_env('BATCH' => 2)
+ end
- context 'in batches of 1' do
- before do
- stub_env('BATCH' => 1)
+ it "enqueues one #{worker_klass} per 2 projects" do
+ projects.map(&:id).sort.each_slice(2) do |first, last|
+ last ||= first
+ expect(worker_klass).to receive(:perform_async).with(first, last)
end
- it 'enqueues one HashedStorage::MigratorWorker per project' do
- projects.each do |project|
- expect(::HashedStorage::MigratorWorker).to receive(:perform_async).with(project.id, project.id)
- end
-
- run_rake_task(task)
- end
+ run_rake_task(task)
end
+ end
+ end
- context 'in batches of 2' do
- before do
- stub_env('BATCH' => 2)
- end
+ describe 'gitlab:storage:migrate_to_hashed' do
+ let(:task) { 'gitlab:storage:migrate_to_hashed' }
- it 'enqueues one HashedStorage::MigratorWorker per 2 projects' do
- projects.map(&:id).sort.each_slice(2) do |first, last|
- last ||= first
- expect(::HashedStorage::MigratorWorker).to receive(:perform_async).with(first, last)
- end
+ context 'with rollback already scheduled' do
+ it 'does nothing' do
+ Sidekiq::Testing.fake! do
+ ::HashedStorage::RollbackerWorker.perform_async(1, 5)
+
+ expect(Project).not_to receive(:with_unmigrated_storage)
- run_rake_task(task)
+ expect { run_rake_task(task) }.to output(/There is already a rollback operation in progress/).to_stderr
end
end
end
+ context 'with 0 legacy projects' do
+ it 'does nothing' do
+ expect(::HashedStorage::MigratorWorker).not_to receive(:perform_async)
+
+ run_rake_task(task)
+ end
+ end
+
+ context 'with 3 legacy projects' do
+ let(:projects) { create_list(:project, 3, :legacy_storage) }
+
+ it_behaves_like "handles custom BATCH env var", ::HashedStorage::MigratorWorker
+ end
+
context 'with same id in range' do
it 'displays message when project cant be found' do
stub_env('ID_FROM', 99999)
@@ -123,6 +141,38 @@ describe 'rake gitlab:storage:*' do
end
end
+ describe 'gitlab:storage:rollback_to_legacy' do
+ let(:task) { 'gitlab:storage:rollback_to_legacy' }
+
+ it_behaves_like 'make sure database is writable'
+
+ context 'with migration already scheduled' do
+ it 'does nothing' do
+ Sidekiq::Testing.fake! do
+ ::HashedStorage::MigratorWorker.perform_async(1, 5)
+
+ expect(Project).not_to receive(:with_unmigrated_storage)
+
+ expect { run_rake_task(task) }.to output(/There is already a migration operation in progress/).to_stderr
+ end
+ end
+ end
+
+ context 'with 0 hashed projects' do
+ it 'does nothing' do
+ expect(::HashedStorage::RollbackerWorker).not_to receive(:perform_async)
+
+ run_rake_task(task)
+ end
+ end
+
+ context 'with 3 hashed projects' do
+ let(:projects) { create_list(:project, 3) }
+
+ it_behaves_like "handles custom BATCH env var", ::HashedStorage::RollbackerWorker
+ end
+ end
+
describe 'gitlab:storage:legacy_projects' do
it_behaves_like 'rake entities summary', 'projects', 'Legacy' do
let(:task) { 'gitlab:storage:legacy_projects' }
diff --git a/spec/validators/sha_validator_spec.rb b/spec/validators/sha_validator_spec.rb
index dc1539cf318..0a76570f65e 100644
--- a/spec/validators/sha_validator_spec.rb
+++ b/spec/validators/sha_validator_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe ShaValidator do
let(:validator) { described_class.new(attributes: [:base_commit_sha]) }
- let(:merge_diff) { build(:merge_request_diff) }
+ let!(:merge_diff) { build(:merge_request_diff) }
subject { validator.validate_each(merge_diff, :base_commit_sha, value) }
@@ -12,6 +12,8 @@ describe ShaValidator do
let(:value) { nil }
it 'does not add any error if value is empty' do
+ expect(Commit).not_to receive(:valid_hash?)
+
subject
expect(merge_diff.errors).to be_empty
@@ -21,7 +23,9 @@ describe ShaValidator do
context 'with valid sha' do
let(:value) { Digest::SHA1.hexdigest(SecureRandom.hex) }
- it 'does not add any error if value is empty' do
+ it 'does not add any error' do
+ expect(Commit).to receive(:valid_hash?).and_call_original
+
subject
expect(merge_diff.errors).to be_empty
@@ -32,6 +36,7 @@ describe ShaValidator do
let(:value) { 'foo' }
it 'adds error to the record' do
+ expect(Commit).to receive(:valid_hash?).and_call_original
expect(merge_diff.errors).to be_empty
subject
diff --git a/spec/views/groups/_home_panel.html.haml_spec.rb b/spec/views/groups/_home_panel.html.haml_spec.rb
new file mode 100644
index 00000000000..91c5ca261b9
--- /dev/null
+++ b/spec/views/groups/_home_panel.html.haml_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'groups/_home_panel' do
+ let(:group) { create(:group) }
+
+ before do
+ assign(:group, group)
+ end
+
+ it 'renders the group ID' do
+ render
+
+ expect(rendered).to have_content("Group ID: #{group.id}")
+ end
+end
diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
index 2a2539c80b5..b52fc719a64 100644
--- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
+++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
@@ -5,6 +5,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
before do
assign :project, project
+ allow(view).to receive(:auto_devops_enabled) { true }
end
it 'shows a warning message about Kubernetes cluster' do
diff --git a/spec/views/projects/settings/operations/show.html.haml_spec.rb b/spec/views/projects/settings/operations/show.html.haml_spec.rb
index 1bca8bba940..6762fe3759b 100644
--- a/spec/views/projects/settings/operations/show.html.haml_spec.rb
+++ b/spec/views/projects/settings/operations/show.html.haml_spec.rb
@@ -18,6 +18,7 @@ describe 'projects/settings/operations/show' do
allow(view).to receive(:error_tracking_setting)
.and_return(error_tracking_setting)
allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:incident_management_available?) { false }
end
let!(:error_tracking_setting) do
diff --git a/vendor/assets/javascripts/jquery.atwho.js b/vendor/assets/javascripts/jquery.atwho.js
deleted file mode 100644
index e058e13303a..00000000000
--- a/vendor/assets/javascripts/jquery.atwho.js
+++ /dev/null
@@ -1,1202 +0,0 @@
-/**
- * at.js - 1.5.1
- * Copyright (c) 2016 chord.luo <chord.luo@gmail.com>;
- * Homepage: http://ichord.github.com/At.js
- * License: MIT
- */
-(function (root, factory) {
- if (typeof define === 'function' && define.amd) {
- // AMD. Register as an anonymous module unless amdModuleId is set
- define(["jquery"], function (a0) {
- return (factory(a0));
- });
- } else if (typeof exports === 'object') {
- // Node. Does not work with strict CommonJS, but
- // only CommonJS-like environments that support module.exports,
- // like Node.
- module.exports = factory(require("jquery"));
- } else {
- factory(jQuery);
- }
-}(this, function ($) {
-var DEFAULT_CALLBACKS, KEY_CODE;
-
-KEY_CODE = {
- DOWN: 40,
- UP: 38,
- ESC: 27,
- TAB: 9,
- ENTER: 13,
- CTRL: 17,
- A: 65,
- P: 80,
- N: 78,
- LEFT: 37,
- UP: 38,
- RIGHT: 39,
- DOWN: 40,
- BACKSPACE: 8,
- SPACE: 32
-};
-
-DEFAULT_CALLBACKS = {
- beforeSave: function(data) {
- return Controller.arrayToDefaultHash(data);
- },
- matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
- var _a, _y, match, regexp, space;
- flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
- if (should_startWithSpace) {
- flag = '(?:^|\\s)' + flag;
- }
- _a = decodeURI("%C3%80");
- _y = decodeURI("%C3%BF");
- space = acceptSpaceBar ? "\ " : "";
- regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi');
- match = regexp.exec(subtext);
- if (match) {
- return match[2] || match[1];
- } else {
- return null;
- }
- },
- filter: function(query, data, searchKey) {
- var _results, i, item, len;
- _results = [];
- for (i = 0, len = data.length; i < len; i++) {
- item = data[i];
- if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) {
- _results.push(item);
- }
- }
- return _results;
- },
- remoteFilter: null,
- sorter: function(query, items, searchKey) {
- var _results, i, item, len;
- if (!query) {
- return items;
- }
- _results = [];
- for (i = 0, len = items.length; i < len; i++) {
- item = items[i];
- item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase());
- if (item.atwho_order > -1) {
- _results.push(item);
- }
- }
- return _results.sort(function(a, b) {
- return a.atwho_order - b.atwho_order;
- });
- },
- tplEval: function(tpl, map) {
- var error, error1, template;
- template = tpl;
- try {
- if (typeof tpl !== 'string') {
- template = tpl(map);
- }
- return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
- return map[key];
- });
- } catch (error1) {
- error = error1;
- return "";
- }
- },
- highlighter: function(li, query) {
- var regexp;
- if (!query) {
- return li;
- }
- regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig');
- return li.replace(regexp, function(str, $1, $2, $3) {
- return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
- });
- },
- beforeInsert: function(value, $li, e) {
- return value;
- },
- beforeReposition: function(offset) {
- return offset;
- },
- afterMatchFailed: function(at, el) {}
-};
-
-var App;
-
-App = (function() {
- function App(inputor) {
- this.currentFlag = null;
- this.controllers = {};
- this.aliasMaps = {};
- this.$inputor = $(inputor);
- this.setupRootElement();
- this.listen();
- }
-
- App.prototype.createContainer = function(doc) {
- var ref;
- if ((ref = this.$el) != null) {
- ref.remove();
- }
- return $(doc.body).append(this.$el = $("<div class='atwho-container'></div>"));
- };
-
- App.prototype.setupRootElement = function(iframe, asRoot) {
- var error, error1;
- if (asRoot == null) {
- asRoot = false;
- }
- if (iframe) {
- this.window = iframe.contentWindow;
- this.document = iframe.contentDocument || this.window.document;
- this.iframe = iframe;
- } else {
- this.document = this.$inputor[0].ownerDocument;
- this.window = this.document.defaultView || this.document.parentWindow;
- try {
- this.iframe = this.window.frameElement;
- } catch (error1) {
- error = error1;
- this.iframe = null;
- if ($.fn.atwho.debug) {
- throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error);
- }
- }
- }
- return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document);
- };
-
- App.prototype.controller = function(at) {
- var c, current, currentFlag, ref;
- if (this.aliasMaps[at]) {
- current = this.controllers[this.aliasMaps[at]];
- } else {
- ref = this.controllers;
- for (currentFlag in ref) {
- c = ref[currentFlag];
- if (currentFlag === at) {
- current = c;
- break;
- }
- }
- }
- if (current) {
- return current;
- } else {
- return this.controllers[this.currentFlag];
- }
- };
-
- App.prototype.setContextFor = function(at) {
- this.currentFlag = at;
- return this;
- };
-
- App.prototype.reg = function(flag, setting) {
- var base, controller;
- controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag));
- if (setting.alias) {
- this.aliasMaps[setting.alias] = flag;
- }
- controller.init(setting);
- return this;
- };
-
- App.prototype.listen = function() {
- return this.$inputor.on('compositionstart', (function(_this) {
- return function(e) {
- var ref;
- if ((ref = _this.controller()) != null) {
- ref.view.hide();
- }
- _this.isComposing = true;
- return null;
- };
- })(this)).on('compositionend', (function(_this) {
- return function(e) {
- _this.isComposing = false;
- setTimeout(function(e) {
- return _this.dispatch(e);
- });
- return null;
- };
- })(this)).on('keyup.atwhoInner', (function(_this) {
- return function(e) {
- return _this.onKeyup(e);
- };
- })(this)).on('keydown.atwhoInner', (function(_this) {
- return function(e) {
- return _this.onKeydown(e);
- };
- })(this)).on('blur.atwhoInner', (function(_this) {
- return function(e) {
- var c;
- if (c = _this.controller()) {
- c.expectedQueryCBId = null;
- return c.view.hide(e, c.getOpt("displayTimeout"));
- }
- };
- })(this)).on('click.atwhoInner', (function(_this) {
- return function(e) {
- return _this.dispatch(e);
- };
- })(this)).on('scroll.atwhoInner', (function(_this) {
- return function() {
- var lastScrollTop;
- lastScrollTop = _this.$inputor.scrollTop();
- return function(e) {
- var currentScrollTop, ref;
- currentScrollTop = e.target.scrollTop;
- if (lastScrollTop !== currentScrollTop) {
- if ((ref = _this.controller()) != null) {
- ref.view.hide(e);
- }
- }
- lastScrollTop = currentScrollTop;
- return true;
- };
- };
- })(this)());
- };
-
- App.prototype.shutdown = function() {
- var _, c, ref;
- ref = this.controllers;
- for (_ in ref) {
- c = ref[_];
- c.destroy();
- delete this.controllers[_];
- }
- this.$inputor.off('.atwhoInner');
- return this.$el.remove();
- };
-
- App.prototype.dispatch = function(e) {
- var _, c, ref, results;
- ref = this.controllers;
- results = [];
- for (_ in ref) {
- c = ref[_];
- results.push(c.lookUp(e));
- }
- return results;
- };
-
- App.prototype.onKeyup = function(e) {
- var ref;
- switch (e.keyCode) {
- case KEY_CODE.ESC:
- e.preventDefault();
- if ((ref = this.controller()) != null) {
- ref.view.hide();
- }
- break;
- case KEY_CODE.DOWN:
- case KEY_CODE.UP:
- case KEY_CODE.CTRL:
- case KEY_CODE.ENTER:
- $.noop();
- break;
- case KEY_CODE.P:
- case KEY_CODE.N:
- if (!e.ctrlKey) {
- this.dispatch(e);
- }
- break;
- default:
- this.dispatch(e);
- }
- };
-
- App.prototype.onKeydown = function(e) {
- var ref, view;
- view = (ref = this.controller()) != null ? ref.view : void 0;
- if (!(view && view.visible())) {
- return;
- }
- switch (e.keyCode) {
- case KEY_CODE.ESC:
- e.preventDefault();
- view.hide(e);
- break;
- case KEY_CODE.UP:
- e.preventDefault();
- view.prev();
- break;
- case KEY_CODE.DOWN:
- e.preventDefault();
- view.next();
- break;
- case KEY_CODE.P:
- if (!e.ctrlKey) {
- return;
- }
- e.preventDefault();
- view.prev();
- break;
- case KEY_CODE.N:
- if (!e.ctrlKey) {
- return;
- }
- e.preventDefault();
- view.next();
- break;
- case KEY_CODE.TAB:
- case KEY_CODE.ENTER:
- case KEY_CODE.SPACE:
- if (!view.visible()) {
- return;
- }
- if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) {
- return;
- }
- if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) {
- return;
- }
- if (view.highlighted()) {
- e.preventDefault();
- view.choose(e);
- } else {
- view.hide(e);
- }
- break;
- default:
- $.noop();
- }
- };
-
- return App;
-
-})();
-
-var Controller,
- slice = [].slice;
-
-Controller = (function() {
- Controller.prototype.uid = function() {
- return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime());
- };
-
- function Controller(app, at1) {
- this.app = app;
- this.at = at1;
- this.$inputor = this.app.$inputor;
- this.id = this.$inputor[0].id || this.uid();
- this.expectedQueryCBId = null;
- this.setting = null;
- this.query = null;
- this.pos = 0;
- this.range = null;
- if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) {
- this.app.$el.append(this.$el = $("<div id='atwho-ground-" + this.id + "'></div>"));
- }
- this.model = new Model(this);
- this.view = new View(this);
- }
-
- Controller.prototype.init = function(setting) {
- this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting);
- this.view.init();
- return this.model.reload(this.setting.data);
- };
-
- Controller.prototype.destroy = function() {
- this.trigger('beforeDestroy');
- this.model.destroy();
- this.view.destroy();
- return this.$el.remove();
- };
-
- Controller.prototype.callDefault = function() {
- var args, error, error1, funcName;
- funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
- try {
- return DEFAULT_CALLBACKS[funcName].apply(this, args);
- } catch (error1) {
- error = error1;
- return $.error(error + " Or maybe At.js doesn't have function " + funcName);
- }
- };
-
- Controller.prototype.trigger = function(name, data) {
- var alias, eventName;
- if (data == null) {
- data = [];
- }
- data.push(this);
- alias = this.getOpt('alias');
- eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho";
- return this.$inputor.trigger(eventName, data);
- };
-
- Controller.prototype.callbacks = function(funcName) {
- return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName];
- };
-
- Controller.prototype.getOpt = function(at, default_value) {
- var e, error1;
- try {
- return this.setting[at];
- } catch (error1) {
- e = error1;
- return null;
- }
- };
-
- Controller.prototype.insertContentFor = function($li) {
- var data, tpl;
- tpl = this.getOpt('insertTpl');
- data = $.extend({}, $li.data('itemData'), {
- 'atwho-at': this.at
- });
- return this.callbacks("tplEval").call(this, tpl, data, "onInsert");
- };
-
- Controller.prototype.renderView = function(data) {
- var searchKey;
- searchKey = this.getOpt("searchKey");
- data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey);
- return this.view.render(data.slice(0, this.getOpt('limit')));
- };
-
- Controller.arrayToDefaultHash = function(data) {
- var i, item, len, results;
- if (!$.isArray(data)) {
- return data;
- }
- results = [];
- for (i = 0, len = data.length; i < len; i++) {
- item = data[i];
- if ($.isPlainObject(item)) {
- results.push(item);
- } else {
- results.push({
- name: item
- });
- }
- }
- return results;
- };
-
- Controller.prototype.lookUp = function(e) {
- var query, wait;
- if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) {
- return;
- }
- if (this.getOpt('suspendOnComposing') && this.app.isComposing) {
- return;
- }
- query = this.catchQuery(e);
- if (!query) {
- this.expectedQueryCBId = null;
- return query;
- }
- this.app.setContextFor(this.at);
- if (wait = this.getOpt('delay')) {
- this._delayLookUp(query, wait);
- } else {
- this._lookUp(query);
- }
- return query;
- };
-
- Controller.prototype._delayLookUp = function(query, wait) {
- var now, remaining;
- now = Date.now ? Date.now() : new Date().getTime();
- this.previousCallTime || (this.previousCallTime = now);
- remaining = wait - (now - this.previousCallTime);
- if ((0 < remaining && remaining < wait)) {
- this.previousCallTime = now;
- this._stopDelayedCall();
- return this.delayedCallTimeout = setTimeout((function(_this) {
- return function() {
- _this.previousCallTime = 0;
- _this.delayedCallTimeout = null;
- return _this._lookUp(query);
- };
- })(this), wait);
- } else {
- this._stopDelayedCall();
- if (this.previousCallTime !== now) {
- this.previousCallTime = 0;
- }
- return this._lookUp(query);
- }
- };
-
- Controller.prototype._stopDelayedCall = function() {
- if (this.delayedCallTimeout) {
- clearTimeout(this.delayedCallTimeout);
- return this.delayedCallTimeout = null;
- }
- };
-
- Controller.prototype._generateQueryCBId = function() {
- return {};
- };
-
- Controller.prototype._lookUp = function(query) {
- var _callback;
- _callback = function(queryCBId, data) {
- if (queryCBId !== this.expectedQueryCBId) {
- return;
- }
- if (data && data.length > 0) {
- return this.renderView(this.constructor.arrayToDefaultHash(data));
- } else {
- return this.view.hide();
- }
- };
- this.expectedQueryCBId = this._generateQueryCBId();
- return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId));
- };
-
- return Controller;
-
-})();
-
-var TextareaController,
- extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
-TextareaController = (function(superClass) {
- extend(TextareaController, superClass);
-
- function TextareaController() {
- return TextareaController.__super__.constructor.apply(this, arguments);
- }
-
- TextareaController.prototype.catchQuery = function() {
- var caretPos, content, end, isString, query, start, subtext;
- content = this.$inputor.val();
- caretPos = this.$inputor.caret('pos', {
- iframe: this.app.iframe
- });
- subtext = content.slice(0, caretPos);
- query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
- isString = typeof query === 'string';
- if (isString && query.length < this.getOpt('minLen', 0)) {
- return;
- }
- if (isString && query.length <= this.getOpt('maxLen', 20)) {
- start = caretPos - query.length;
- end = start + query.length;
- this.pos = start;
- query = {
- 'text': query,
- 'headPos': start,
- 'endPos': end
- };
- this.trigger("matched", [this.at, query.text]);
- } else {
- query = null;
- this.view.hide();
- }
- return this.query = query;
- };
-
- TextareaController.prototype.rect = function() {
- var c, iframeOffset, scaleBottom;
- if (!(c = this.$inputor.caret('offset', this.pos - 1, {
- iframe: this.app.iframe
- }))) {
- return;
- }
- if (this.app.iframe && !this.app.iframeAsRoot) {
- iframeOffset = $(this.app.iframe).offset();
- c.left += iframeOffset.left;
- c.top += iframeOffset.top;
- }
- scaleBottom = this.app.document.selection ? 0 : 2;
- return {
- left: c.left,
- top: c.top,
- bottom: c.top + c.height + scaleBottom
- };
- };
-
- TextareaController.prototype.insert = function(content, $li) {
- var $inputor, source, startStr, suffix, text;
- $inputor = this.$inputor;
- source = $inputor.val();
- startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0));
- suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " ";
- content += suffix;
- text = "" + startStr + content + (source.slice(this.query['endPos'] || 0));
- $inputor.val(text);
- $inputor.caret('pos', startStr.length + content.length, {
- iframe: this.app.iframe
- });
- if (!$inputor.is(':focus')) {
- $inputor.focus();
- }
- return $inputor.change();
- };
-
- return TextareaController;
-
-})(Controller);
-
-var EditableController,
- extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
-EditableController = (function(superClass) {
- extend(EditableController, superClass);
-
- function EditableController() {
- return EditableController.__super__.constructor.apply(this, arguments);
- }
-
- EditableController.prototype._getRange = function() {
- var sel;
- sel = this.app.window.getSelection();
- if (sel.rangeCount > 0) {
- return sel.getRangeAt(0);
- }
- };
-
- EditableController.prototype._setRange = function(position, node, range) {
- if (range == null) {
- range = this._getRange();
- }
- if (!range) {
- return;
- }
- node = $(node)[0];
- if (position === 'after') {
- range.setEndAfter(node);
- range.setStartAfter(node);
- } else {
- range.setEndBefore(node);
- range.setStartBefore(node);
- }
- range.collapse(false);
- return this._clearRange(range);
- };
-
- EditableController.prototype._clearRange = function(range) {
- var sel;
- if (range == null) {
- range = this._getRange();
- }
- sel = this.app.window.getSelection();
- if (this.ctrl_a_pressed == null) {
- sel.removeAllRanges();
- return sel.addRange(range);
- }
- };
-
- EditableController.prototype._movingEvent = function(e) {
- var ref;
- return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN);
- };
-
- EditableController.prototype._unwrap = function(node) {
- var next;
- node = $(node).unwrap().get(0);
- if ((next = node.nextSibling) && next.nodeValue) {
- node.nodeValue += next.nodeValue;
- $(next).remove();
- }
- return node;
- };
-
- EditableController.prototype.catchQuery = function(e) {
- var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range;
- if (!(range = this._getRange())) {
- return;
- }
- if (!range.collapsed) {
- return;
- }
- if (e.which === KEY_CODE.ENTER) {
- ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap();
- if ($query.is(':empty')) {
- $query.remove();
- }
- ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap();
- this._clearRange();
- return;
- }
- if (/firefox/i.test(navigator.userAgent)) {
- if ($(range.startContainer).is(this.$inputor)) {
- this._clearRange();
- return;
- }
- if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) {
- _range = range.cloneRange();
- _range.setStart(range.startContainer, offset);
- if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) {
- inserted = $(range.startContainer).contents().get(offset);
- this._setRange('after', $(inserted).contents().last());
- }
- } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) {
- $inserted = $(range.startContainer.previousSibling);
- if ($inserted.is('.atwho-inserted') && range.startOffset === 0) {
- this._setRange('after', $inserted.contents().last());
- }
- }
- }
- $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query');
- if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) {
- $query.remove();
- }
- if (!this._movingEvent(e)) {
- $query.removeClass('atwho-inserted');
- }
- if ($query.length > 0) {
- switch (e.which) {
- case KEY_CODE.LEFT:
- this._setRange('before', $query.get(0), range);
- $query.removeClass('atwho-query');
- return;
- case KEY_CODE.RIGHT:
- this._setRange('after', $query.get(0).nextSibling, range);
- $query.removeClass('atwho-query');
- return;
- }
- }
- if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) {
- $query.empty().html(query_content).attr('data-atwho-at-query', null);
- this._setRange('after', $query.get(0), range);
- }
- _range = range.cloneRange();
- _range.setStart(range.startContainer, 0);
- matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar"));
- isString = typeof matched === 'string';
- if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) {
- range.setStart(range.startContainer, index);
- $query = $('<span/>', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query');
- range.surroundContents($query.get(0));
- lastNode = $query.contents().last().get(0);
- if (/firefox/i.test(navigator.userAgent)) {
- range.setStart(lastNode, lastNode.length);
- range.setEnd(lastNode, lastNode.length);
- this._clearRange(range);
- } else {
- this._setRange('after', lastNode, range);
- }
- }
- if (isString && matched.length < this.getOpt('minLen', 0)) {
- return;
- }
- if (isString && matched.length <= this.getOpt('maxLen', 20)) {
- query = {
- text: matched,
- el: $query
- };
- this.trigger("matched", [this.at, query.text]);
- return this.query = query;
- } else {
- this.view.hide();
- this.query = {
- el: $query
- };
- if ($query.text().indexOf(this.at) >= 0) {
- if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) {
- $query.removeClass('atwho-query');
- } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) {
- this._setRange("after", this._unwrap($query.text($query.text()).contents().first()));
- }
- }
- return null;
- }
- };
-
- EditableController.prototype.rect = function() {
- var $iframe, iframeOffset, rect;
- rect = this.query.el.offset();
- if (this.app.iframe && !this.app.iframeAsRoot) {
- iframeOffset = ($iframe = $(this.app.iframe)).offset();
- rect.left += iframeOffset.left - this.$inputor.scrollLeft();
- rect.top += iframeOffset.top - this.$inputor.scrollTop();
- }
- rect.bottom = rect.top + this.query.el.height();
- return rect;
- };
-
- EditableController.prototype.insert = function(content, $li) {
- var data, range, suffix, suffixNode;
- if (!this.$inputor.is(':focus')) {
- this.$inputor.focus();
- }
- suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0";
- data = $li.data('itemData');
- this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text);
- if (range = this._getRange()) {
- range.setEndAfter(this.query.el[0]);
- range.collapse(false);
- range.insertNode(suffixNode = this.app.document.createTextNode("\u200D" + suffix));
- this._setRange('after', suffixNode, range);
- }
- if (!this.$inputor.is(':focus')) {
- this.$inputor.focus();
- }
- return this.$inputor.change();
- };
-
- return EditableController;
-
-})(Controller);
-
-var Model;
-
-Model = (function() {
- function Model(context) {
- this.context = context;
- this.at = this.context.at;
- this.storage = this.context.$inputor;
- }
-
- Model.prototype.destroy = function() {
- return this.storage.data(this.at, null);
- };
-
- Model.prototype.saved = function() {
- return this.fetch() > 0;
- };
-
- Model.prototype.query = function(query, callback) {
- var _remoteFilter, data, searchKey;
- data = this.fetch();
- searchKey = this.context.getOpt("searchKey");
- data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || [];
- _remoteFilter = this.context.callbacks('remoteFilter');
- if (data.length > 0 || (!_remoteFilter && data.length === 0)) {
- return callback(data);
- } else {
- return _remoteFilter.call(this.context, query, callback);
- }
- };
-
- Model.prototype.fetch = function() {
- return this.storage.data(this.at) || [];
- };
-
- Model.prototype.save = function(data) {
- return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || []));
- };
-
- Model.prototype.load = function(data) {
- if (!(this.saved() || !data)) {
- return this._load(data);
- }
- };
-
- Model.prototype.reload = function(data) {
- return this._load(data);
- };
-
- Model.prototype._load = function(data) {
- if (typeof data === "string") {
- return $.ajax(data, {
- dataType: "json"
- }).done((function(_this) {
- return function(data) {
- return _this.save(data);
- };
- })(this));
- } else {
- return this.save(data);
- }
- };
-
- return Model;
-
-})();
-
-var View;
-
-View = (function() {
- function View(context) {
- this.context = context;
- this.$el = $("<div class='atwho-view'><ul class='atwho-view-ul'></ul></div>");
- this.$elUl = this.$el.children();
- this.timeoutID = null;
- this.context.$el.append(this.$el);
- this.bindEvent();
- }
-
- View.prototype.init = function() {
- var header_tpl, id;
- id = this.context.getOpt("alias") || this.context.at.charCodeAt(0);
- header_tpl = this.context.getOpt("headerTpl");
- if (header_tpl && this.$el.children().length === 1) {
- this.$el.prepend(header_tpl);
- }
- return this.$el.attr({
- 'id': "at-view-" + id
- });
- };
-
- View.prototype.destroy = function() {
- return this.$el.remove();
- };
-
- View.prototype.bindEvent = function() {
- var $menu, lastCoordX, lastCoordY;
- $menu = this.$el.find('ul');
- lastCoordX = 0;
- lastCoordY = 0;
- return $menu.on('mousemove.atwho-view', 'li', (function(_this) {
- return function(e) {
- var $cur;
- if (lastCoordX === e.clientX && lastCoordY === e.clientY) {
- return;
- }
- lastCoordX = e.clientX;
- lastCoordY = e.clientY;
- $cur = $(e.currentTarget);
- if ($cur.hasClass('cur')) {
- return;
- }
- $menu.find('.cur').removeClass('cur');
- return $cur.addClass('cur');
- };
- })(this)).on('click.atwho-view', 'li', (function(_this) {
- return function(e) {
- $menu.find('.cur').removeClass('cur');
- $(e.currentTarget).addClass('cur');
- _this.choose(e);
- return e.preventDefault();
- };
- })(this));
- };
-
- View.prototype.visible = function() {
- return this.$el.is(":visible");
- };
-
- View.prototype.highlighted = function() {
- return this.$el.find(".cur").length > 0;
- };
-
- View.prototype.choose = function(e) {
- var $li, content;
- if (($li = this.$el.find(".cur")).length) {
- content = this.context.insertContentFor($li);
- this.context._stopDelayedCall();
- this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li, e), $li);
- this.context.trigger("inserted", [$li, e]);
- this.hide(e);
- }
- if (this.context.getOpt("hideWithoutSuffix")) {
- return this.stopShowing = true;
- }
- };
-
- View.prototype.reposition = function(rect) {
- var _window, offset, overflowOffset, ref;
- _window = this.context.app.iframeAsRoot ? this.context.app.window : window;
- if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) {
- rect.bottom = rect.top - this.$el.height();
- }
- if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) {
- rect.left = overflowOffset;
- }
- offset = {
- left: rect.left,
- top: rect.bottom
- };
- if ((ref = this.context.callbacks("beforeReposition")) != null) {
- ref.call(this.context, offset);
- }
- this.$el.offset(offset);
- return this.context.trigger("reposition", [offset]);
- };
-
- View.prototype.next = function() {
- var cur, next, nextEl, offset;
- cur = this.$el.find('.cur').removeClass('cur');
- next = cur.next();
- if (!next.length) {
- next = this.$el.find('li:first');
- }
- next.addClass('cur');
- nextEl = next[0];
- offset = nextEl.offsetTop + nextEl.offsetHeight + (nextEl.nextSibling ? nextEl.nextSibling.offsetHeight : 0);
- return this.scrollTop(Math.max(0, offset - this.$el.height()));
- };
-
- View.prototype.prev = function() {
- var cur, offset, prev, prevEl;
- cur = this.$el.find('.cur').removeClass('cur');
- prev = cur.prev();
- if (!prev.length) {
- prev = this.$el.find('li:last');
- }
- prev.addClass('cur');
- prevEl = prev[0];
- offset = prevEl.offsetTop + prevEl.offsetHeight + (prevEl.nextSibling ? prevEl.nextSibling.offsetHeight : 0);
- return this.scrollTop(Math.max(0, offset - this.$el.height()));
- };
-
- View.prototype.scrollTop = function(scrollTop) {
- var scrollDuration;
- scrollDuration = this.context.getOpt('scrollDuration');
- if (scrollDuration) {
- return this.$elUl.animate({
- scrollTop: scrollTop
- }, scrollDuration);
- } else {
- return this.$elUl.scrollTop(scrollTop);
- }
- };
-
- View.prototype.show = function() {
- var rect;
- if (this.stopShowing) {
- this.stopShowing = false;
- return;
- }
- if (!this.visible()) {
- this.$el.show();
- this.$el.scrollTop(0);
- this.context.trigger('shown');
- }
- if (rect = this.context.rect()) {
- return this.reposition(rect);
- }
- };
-
- View.prototype.hide = function(e, time) {
- var callback;
- if (!this.visible()) {
- return;
- }
- if (isNaN(time)) {
- this.$el.hide();
- return this.context.trigger('hidden', [e]);
- } else {
- callback = (function(_this) {
- return function() {
- return _this.hide();
- };
- })(this);
- clearTimeout(this.timeoutID);
- return this.timeoutID = setTimeout(callback, time);
- }
- };
-
- View.prototype.render = function(list) {
- var $li, $ul, i, item, len, li, tpl;
- if (!($.isArray(list) && list.length > 0)) {
- this.hide();
- return;
- }
- this.$el.find('ul').empty();
- $ul = this.$el.find('ul');
- tpl = this.context.getOpt('displayTpl');
- for (i = 0, len = list.length; i < len; i++) {
- item = list[i];
- item = $.extend({}, item, {
- 'atwho-at': this.context.at
- });
- li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay");
- $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text));
- $li.data("item-data", item);
- $ul.append($li);
- }
- this.show();
- if (this.context.getOpt('highlightFirst')) {
- return $ul.find("li:first").addClass("cur");
- }
- };
-
- return View;
-
-})();
-
-var Api;
-
-Api = {
- load: function(at, data) {
- var c;
- if (c = this.controller(at)) {
- return c.model.load(data);
- }
- },
- isSelecting: function() {
- var ref;
- return !!((ref = this.controller()) != null ? ref.view.visible() : void 0);
- },
- hide: function() {
- var ref;
- return (ref = this.controller()) != null ? ref.view.hide() : void 0;
- },
- reposition: function() {
- var c;
- if (c = this.controller()) {
- return c.view.reposition(c.rect());
- }
- },
- setIframe: function(iframe, asRoot) {
- this.setupRootElement(iframe, asRoot);
- return null;
- },
- run: function() {
- return this.dispatch();
- },
- destroy: function() {
- this.shutdown();
- return this.$inputor.data('atwho', null);
- }
-};
-
-$.fn.atwho = function(method) {
- var _args, result;
- _args = arguments;
- result = null;
- this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() {
- var $this, app;
- if (!(app = ($this = $(this)).data("atwho"))) {
- $this.data('atwho', (app = new App(this)));
- }
- if (typeof method === 'object' || !method) {
- return app.reg(method.at, method);
- } else if (Api[method] && app) {
- return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1));
- } else {
- return $.error("Method " + method + " does not exist on jQuery.atwho");
- }
- });
- if (result != null) {
- return result;
- } else {
- return this;
- }
-};
-
-$.fn.atwho["default"] = {
- at: void 0,
- alias: void 0,
- data: null,
- displayTpl: "<li>${name}</li>",
- insertTpl: "${atwho-at}${name}",
- headerTpl: null,
- callbacks: DEFAULT_CALLBACKS,
- searchKey: "name",
- suffix: void 0,
- hideWithoutSuffix: false,
- startWithSpace: true,
- acceptSpaceBar: false,
- highlightFirst: true,
- limit: 5,
- maxLen: 20,
- minLen: 0,
- displayTimeout: 300,
- delay: null,
- spaceSelectsMatch: false,
- tabSelectsMatch: true,
- editableAtwhoQueryAttrs: {},
- scrollDuration: 150,
- suspendOnComposing: true,
- lookUpOnClick: true
-};
-
-$.fn.atwho.debug = false;
-
-}));
diff --git a/vendor/assets/javascripts/jquery.caret.js b/vendor/assets/javascripts/jquery.caret.js
deleted file mode 100644
index 811ec63ee47..00000000000
--- a/vendor/assets/javascripts/jquery.caret.js
+++ /dev/null
@@ -1,436 +0,0 @@
-(function (root, factory) {
- if (typeof define === 'function' && define.amd) {
- // AMD. Register as an anonymous module.
- define(["jquery"], function ($) {
- return (root.returnExportsGlobal = factory($));
- });
- } else if (typeof exports === 'object') {
- // Node. Does not work with strict CommonJS, but
- // only CommonJS-like enviroments that support module.exports,
- // like Node.
- module.exports = factory(require("jquery"));
- } else {
- factory(jQuery);
- }
-}(this, function ($) {
-
-/*
- Implement Github like autocomplete mentions
- http://ichord.github.com/At.js
-
- Copyright (c) 2013 chord.luo@gmail.com
- Licensed under the MIT license.
-*/
-
-/*
-本插件操作 textarea 或者 input 内的插入符
-只实现了获得插入符在文本框中的位置,我设置
-插入符的位置.
-*/
-
-"use strict";
-var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy;
-
-pluginName = 'caret';
-
-EditableCaret = (function() {
- function EditableCaret($inputor) {
- this.$inputor = $inputor;
- this.domInputor = this.$inputor[0];
- }
-
- EditableCaret.prototype.setPos = function(pos) {
- var fn, found, offset, sel;
- if (sel = oWindow.getSelection()) {
- offset = 0;
- found = false;
- (fn = function(pos, parent) {
- var node, range, _i, _len, _ref, _results;
- _ref = parent.childNodes;
- _results = [];
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- node = _ref[_i];
- if (found) {
- break;
- }
- if (node.nodeType === 3) {
- if (offset + node.length >= pos) {
- found = true;
- range = oDocument.createRange();
- range.setStart(node, pos - offset);
- sel.removeAllRanges();
- sel.addRange(range);
- break;
- } else {
- _results.push(offset += node.length);
- }
- } else {
- _results.push(fn(pos, node));
- }
- }
- return _results;
- })(pos, this.domInputor);
- }
- return this.domInputor;
- };
-
- EditableCaret.prototype.getIEPosition = function() {
- return this.getPosition();
- };
-
- EditableCaret.prototype.getPosition = function() {
- var inputor_offset, offset;
- offset = this.getOffset();
- inputor_offset = this.$inputor.offset();
- offset.left -= inputor_offset.left;
- offset.top -= inputor_offset.top;
- return offset;
- };
-
- EditableCaret.prototype.getOldIEPos = function() {
- var preCaretTextRange, textRange;
- textRange = oDocument.selection.createRange();
- preCaretTextRange = oDocument.body.createTextRange();
- preCaretTextRange.moveToElementText(this.domInputor);
- preCaretTextRange.setEndPoint("EndToEnd", textRange);
- return preCaretTextRange.text.length;
- };
-
- EditableCaret.prototype.getPos = function() {
- var clonedRange, pos, range;
- if (range = this.range()) {
- clonedRange = range.cloneRange();
- clonedRange.selectNodeContents(this.domInputor);
- clonedRange.setEnd(range.endContainer, range.endOffset);
- pos = clonedRange.toString().length;
- clonedRange.detach();
- return pos;
- } else if (oDocument.selection) {
- return this.getOldIEPos();
- }
- };
-
- EditableCaret.prototype.getOldIEOffset = function() {
- var range, rect;
- range = oDocument.selection.createRange().duplicate();
- range.moveStart("character", -1);
- rect = range.getBoundingClientRect();
- return {
- height: rect.bottom - rect.top,
- left: rect.left,
- top: rect.top
- };
- };
-
- EditableCaret.prototype.getOffset = function(pos) {
- var clonedRange, offset, range, rect, shadowCaret;
- if (oWindow.getSelection && (range = this.range())) {
- if (range.endOffset - 1 > 0 && range.endContainer !== this.domInputor) {
- clonedRange = range.cloneRange();
- clonedRange.setStart(range.endContainer, range.endOffset - 1);
- clonedRange.setEnd(range.endContainer, range.endOffset);
- rect = clonedRange.getBoundingClientRect();
- offset = {
- height: rect.height,
- left: rect.left + rect.width,
- top: rect.top
- };
- clonedRange.detach();
- }
- if (!offset || (offset != null ? offset.height : void 0) === 0) {
- clonedRange = range.cloneRange();
- shadowCaret = $(oDocument.createTextNode("|"));
- clonedRange.insertNode(shadowCaret[0]);
- clonedRange.selectNode(shadowCaret[0]);
- rect = clonedRange.getBoundingClientRect();
- offset = {
- height: rect.height,
- left: rect.left,
- top: rect.top
- };
- shadowCaret.remove();
- clonedRange.detach();
- }
- } else if (oDocument.selection) {
- offset = this.getOldIEOffset();
- }
- if (offset) {
- offset.top += $(oWindow).scrollTop();
- offset.left += $(oWindow).scrollLeft();
- }
- return offset;
- };
-
- EditableCaret.prototype.range = function() {
- var sel;
- if (!oWindow.getSelection) {
- return;
- }
- sel = oWindow.getSelection();
- if (sel.rangeCount > 0) {
- return sel.getRangeAt(0);
- } else {
- return null;
- }
- };
-
- return EditableCaret;
-
-})();
-
-InputCaret = (function() {
- function InputCaret($inputor) {
- this.$inputor = $inputor;
- this.domInputor = this.$inputor[0];
- }
-
- InputCaret.prototype.getIEPos = function() {
- var endRange, inputor, len, normalizedValue, pos, range, textInputRange;
- inputor = this.domInputor;
- range = oDocument.selection.createRange();
- pos = 0;
- if (range && range.parentElement() === inputor) {
- normalizedValue = inputor.value.replace(/\r\n/g, "\n");
- len = normalizedValue.length;
- textInputRange = inputor.createTextRange();
- textInputRange.moveToBookmark(range.getBookmark());
- endRange = inputor.createTextRange();
- endRange.collapse(false);
- if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
- pos = len;
- } else {
- pos = -textInputRange.moveStart("character", -len);
- }
- }
- return pos;
- };
-
- InputCaret.prototype.getPos = function() {
- if (oDocument.selection) {
- return this.getIEPos();
- } else {
- return this.domInputor.selectionStart;
- }
- };
-
- InputCaret.prototype.setPos = function(pos) {
- var inputor, range;
- inputor = this.domInputor;
- if (oDocument.selection) {
- range = inputor.createTextRange();
- range.move("character", pos);
- range.select();
- } else if (inputor.setSelectionRange) {
- inputor.setSelectionRange(pos, pos);
- }
- return inputor;
- };
-
- InputCaret.prototype.getIEOffset = function(pos) {
- var h, textRange, x, y;
- textRange = this.domInputor.createTextRange();
- pos || (pos = this.getPos());
- textRange.move('character', pos);
- x = textRange.boundingLeft;
- y = textRange.boundingTop;
- h = textRange.boundingHeight;
- return {
- left: x,
- top: y,
- height: h
- };
- };
-
- InputCaret.prototype.getOffset = function(pos) {
- var $inputor, offset, position;
- $inputor = this.$inputor;
- if (oDocument.selection) {
- offset = this.getIEOffset(pos);
- offset.top += $(oWindow).scrollTop() + $inputor.scrollTop();
- offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft();
- return offset;
- } else {
- offset = $inputor.offset();
- position = this.getPosition(pos);
- return offset = {
- left: offset.left + position.left - $inputor.scrollLeft(),
- top: offset.top + position.top - $inputor.scrollTop(),
- height: position.height
- };
- }
- };
-
- InputCaret.prototype.getPosition = function(pos) {
- var $inputor, at_rect, end_range, format, html, mirror, start_range;
- $inputor = this.$inputor;
- format = function(value) {
- value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "<br/>");
- if (/firefox/i.test(navigator.userAgent)) {
- value = value.replace(/\s/g, '&nbsp;');
- }
- return value;
- };
- if (pos === void 0) {
- pos = this.getPos();
- }
- start_range = $inputor.val().slice(0, pos);
- end_range = $inputor.val().slice(pos);
- html = "<span style='position: relative; display: inline;'>" + format(start_range) + "</span>";
- html += "<span id='caret' style='position: relative; display: inline;'>|</span>";
- html += "<span style='position: relative; display: inline;'>" + format(end_range) + "</span>";
- mirror = new Mirror($inputor);
- return at_rect = mirror.create(html).rect();
- };
-
- InputCaret.prototype.getIEPosition = function(pos) {
- var h, inputorOffset, offset, x, y;
- offset = this.getIEOffset(pos);
- inputorOffset = this.$inputor.offset();
- x = offset.left - inputorOffset.left;
- y = offset.top - inputorOffset.top;
- h = offset.height;
- return {
- left: x,
- top: y,
- height: h
- };
- };
-
- return InputCaret;
-
-})();
-
-Mirror = (function() {
- Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"];
-
- function Mirror($inputor) {
- this.$inputor = $inputor;
- }
-
- Mirror.prototype.mirrorCss = function() {
- var css,
- _this = this;
- css = {
- position: 'absolute',
- left: -9999,
- top: 0,
- zIndex: -20000
- };
- if (this.$inputor.prop('tagName') === 'TEXTAREA') {
- this.css_attr.push('width');
- }
- $.each(this.css_attr, function(i, p) {
- return css[p] = _this.$inputor.css(p);
- });
- return css;
- };
-
- Mirror.prototype.create = function(html) {
- this.$mirror = $('<div></div>');
- this.$mirror.css(this.mirrorCss());
- this.$mirror.html(html);
- this.$inputor.after(this.$mirror);
- return this;
- };
-
- Mirror.prototype.rect = function() {
- var $flag, pos, rect;
- $flag = this.$mirror.find("#caret");
- pos = $flag.position();
- rect = {
- left: pos.left,
- top: pos.top,
- height: $flag.height()
- };
- this.$mirror.remove();
- return rect;
- };
-
- return Mirror;
-
-})();
-
-Utils = {
- contentEditable: function($inputor) {
- return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true');
- }
-};
-
-methods = {
- pos: function(pos) {
- if (pos || pos === 0) {
- return this.setPos(pos);
- } else {
- return this.getPos();
- }
- },
- position: function(pos) {
- if (oDocument.selection) {
- return this.getIEPosition(pos);
- } else {
- return this.getPosition(pos);
- }
- },
- offset: function(pos) {
- var offset;
- offset = this.getOffset(pos);
- return offset;
- }
-};
-
-oDocument = null;
-
-oWindow = null;
-
-oFrame = null;
-
-setContextBy = function(settings) {
- var iframe;
- if (iframe = settings != null ? settings.iframe : void 0) {
- oFrame = iframe;
- oWindow = iframe.contentWindow;
- return oDocument = iframe.contentDocument || oWindow.document;
- } else {
- oFrame = void 0;
- oWindow = window;
- return oDocument = document;
- }
-};
-
-discoveryIframeOf = function($dom) {
- var error;
- oDocument = $dom[0].ownerDocument;
- oWindow = oDocument.defaultView || oDocument.parentWindow;
- try {
- return oFrame = oWindow.frameElement;
- } catch (_error) {
- error = _error;
- }
-};
-
-$.fn.caret = function(method, value, settings) {
- var caret;
- if (methods[method]) {
- if ($.isPlainObject(value)) {
- setContextBy(value);
- value = void 0;
- } else {
- setContextBy(settings);
- }
- caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this);
- return methods[method].apply(caret, [value]);
- } else {
- return $.error("Method " + method + " does not exist on jQuery.caret");
- }
-};
-
-$.fn.caret.EditableCaret = EditableCaret;
-
-$.fn.caret.InputCaret = InputCaret;
-
-$.fn.caret.Utils = Utils;
-
-$.fn.caret.apis = methods;
-
-
-}));
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index ed79ec5bac3..de6e32cb998 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -606,7 +606,6 @@ iterall,1.2.2,MIT
jed,1.1.1,MIT
jira-ruby,1.4.1,MIT
jquery,3.3.1,MIT
-jquery-atwho-rails,1.3.2,MIT
jquery-ujs,1.2.2,MIT
jquery.waitforimages,2.2.0,MIT
js-beautify,1.8.8,MIT
diff --git a/yarn.lock b/yarn.lock
index 1ac6b322469..dbd56b387ba 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -663,10 +663,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.54.0.tgz#00320e845efd46716042cde0c348b990d4908daf"
integrity sha512-DR17iy8TM5IbXEacqiDP0p8SuC/J8EL+98xbfVz5BKvRsPHpeZJQNlBF/petIV5d+KWM5A9v3GZTY7uMU7z/JQ==
-"@gitlab/ui@^2.2.2":
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.2.2.tgz#de8f436d6b52c168c4752b221a88cf7665fe0134"
- integrity sha512-xVY8lGfDA3D2EtyfZVpJVeNUXLbpn/xJqNF4fleqHJfqrwt+IcVlsQ7yEs/LBukmIw6tXPliD4Mm9uefnQhkXA==
+"@gitlab/ui@^2.2.3":
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.2.3.tgz#b3b4d1d785662dfba44ad2a7354fcbe4cee22fc9"
+ integrity sha512-N1Q1O6zpS4zZhmvWsUCtuGXTzQeHOzRWQZctbFTEJonidIWk6juqIBduYgR0MadG3DZxiovUN12jDGVtCfZKzw==
dependencies:
"@babel/standalone" "^7.0.0"
bootstrap-vue "^2.0.0-rc.11"
@@ -1378,6 +1378,11 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+at.js@^1.5.4:
+ version "1.5.4"
+ resolved "https://registry.yarnpkg.com/at.js/-/at.js-1.5.4.tgz#8fc60cc80eadbe4874449b166a818e7ae1d784c1"
+ integrity sha512-G8mgUb/PqShPoH8AyjuxsTGvIr1o716BtQUKDM44C8qN2W615y7KGJ68MlTGamd0J0D/m28emUkzagaHTdrGZw==
+
atob@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
@@ -6070,6 +6075,11 @@ jquery-ujs@1.2.2:
dependencies:
jquery ">=1.8.0"
+jquery.caret@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/jquery.caret/-/jquery.caret-0.3.1.tgz#9c093318faf327eff322e826ca9f3241368bc7b8"
+ integrity sha1-nAkzGPrzJ+/zIugmyp8yQTaLx7g=
+
jquery.waitforimages@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/jquery.waitforimages/-/jquery.waitforimages-2.2.0.tgz#63f23131055a1b060dc913e6d874bcc9b9e6b16b"
@@ -8086,16 +8096,16 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
-prettier@1.16.1:
- version "1.16.1"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.1.tgz#534c2c9d7853f8845e5e078384e71973bd74089f"
- integrity sha512-XXUITwIkGb3CPJ2hforHah/zTINRyie5006Jd2HKy2qz7snEJXl0KLfsJZW/wst9g6R2rFvqba3VpNYdu1hDcA==
-
prettier@1.16.3:
version "1.16.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d"
integrity sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw==
+prettier@1.16.4:
+ version "1.16.4"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717"
+ integrity sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==
+
pretty-format@^24.0.0:
version "24.0.0"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.0.0.tgz#cb6599fd73ac088e37ed682f61291e4678f48591"