summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.codeclimate.yml58
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml5
-rw-r--r--.gitlab/issue_templates/Bug.md6
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md4
-rw-r--r--.haml-lint.yml2
-rw-r--r--.pkgr.yml4
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_ELASTICSEARCH_INDEXER_VERSION1
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile11
-rw-r--r--Gemfile.lock52
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js2
-rw-r--r--app/assets/javascripts/boards/stores/actions.js65
-rw-r--r--app/assets/javascripts/boards/stores/index.js14
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js21
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js91
-rw-r--r--app/assets/javascripts/boards/stores/state.js3
-rw-r--r--app/assets/javascripts/contextual_sidebar.js3
-rw-r--r--app/assets/javascripts/diffs/components/app.vue12
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue9
-rw-r--r--app/assets/javascripts/diffs/store/actions.js9
-rw-r--r--app/assets/javascripts/fly_out_nav.js3
-rw-r--r--app/assets/javascripts/gl_field_error.js3
-rw-r--r--app/assets/javascripts/gpg_badges.js2
-rw-r--r--app/assets/javascripts/groups_select.js3
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js3
-rw-r--r--app/assets/javascripts/issuable_index.js8
-rw-r--r--app/assets/javascripts/issue.js11
-rw-r--r--app/assets/javascripts/issue_status_select.js3
-rw-r--r--app/assets/javascripts/lib/utils/highlight.js6
-rw-r--r--app/assets/javascripts/main.js7
-rw-r--r--app/assets/javascripts/merge_request_tabs.js5
-rw-r--r--app/assets/javascripts/milestone.js3
-rw-r--r--app/assets/javascripts/milestone_select.js15
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js3
-rw-r--r--app/assets/javascripts/namespace_select.js3
-rw-r--r--app/assets/javascripts/notes.js42
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/tree/show/index.js8
-rw-r--r--app/assets/javascripts/reports/components/issues_list.vue6
-rw-r--r--app/assets/javascripts/reports/components/report_item.vue12
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue6
-rw-r--r--app/assets/javascripts/reports/store/state.js5
-rw-r--r--app/assets/javascripts/repository/components/app.vue3
-rw-r--r--app/assets/javascripts/repository/graphql.js11
-rw-r--r--app/assets/javascripts/repository/index.js25
-rw-r--r--app/assets/javascripts/repository/pages/index.vue24
-rw-r--r--app/assets/javascripts/repository/pages/tree.vue15
-rw-r--r--app/assets/javascripts/repository/queries/getRef.graphql3
-rw-r--r--app/assets/javascripts/repository/router.js36
-rw-r--r--app/assets/stylesheets/components/toast.scss54
-rw-r--r--app/assets/stylesheets/framework/blocks.scss14
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss16
-rw-r--r--app/assets/stylesheets/framework/variables.scss13
-rw-r--r--app/assets/stylesheets/pages/members.scss2
-rw-r--r--app/controllers/admin/application_settings_controller.rb7
-rw-r--r--app/controllers/concerns/project_unauthorized.rb14
-rw-r--r--app/controllers/concerns/routable_actions.rb18
-rw-r--r--app/controllers/projects/application_controller.rb3
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb4
-rw-r--r--app/controllers/projects/clusters_controller.rb4
-rw-r--r--app/controllers/projects/environments_controller.rb1
-rw-r--r--app/controllers/projects/serverless/functions_controller.rb2
-rw-r--r--app/controllers/registrations_controller.rb25
-rw-r--r--app/controllers/uploads_controller.rb2
-rw-r--r--app/graphql/resolvers/issues_resolver.rb2
-rw-r--r--app/helpers/dashboard_helper.rb4
-rw-r--r--app/helpers/emails_helper.rb22
-rw-r--r--app/helpers/events_helper.rb12
-rw-r--r--app/helpers/nav_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/storage_helper.rb2
-rw-r--r--app/mailers/devise_mailer.rb1
-rw-r--r--app/mailers/emails/issues.rb7
-rw-r--r--app/mailers/emails/merge_requests.rb4
-rw-r--r--app/models/active_session.rb49
-rw-r--r--app/models/application_record.rb4
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/clusters/applications/jupyter.rb2
-rw-r--r--app/models/clusters/concerns/application_data.rb74
-rw-r--r--app/models/event.rb54
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/project.rb9
-rw-r--r--app/models/push_event.rb2
-rw-r--r--app/models/repository.rb27
-rw-r--r--app/serializers/test_case_entity.rb1
-rw-r--r--app/services/ci/register_job_service.rb5
-rw-r--r--app/services/clusters/refresh_service.rb6
-rw-r--r--app/services/concerns/users/participable_service.rb2
-rw-r--r--app/services/git/base_hooks_service.rb9
-rw-r--r--app/services/git/branch_push_service.rb1
-rw-r--r--app/services/issues/close_service.rb13
-rw-r--r--app/services/members/destroy_service.rb2
-rw-r--r--app/services/notification_service.rb8
-rw-r--r--app/services/projects/create_service.rb6
-rw-r--r--app/services/projects/transfer_service.rb5
-rw-r--r--app/uploaders/attachment_uploader.rb2
-rw-r--r--app/uploaders/avatar_uploader.rb2
-rw-r--r--app/uploaders/file_uploader.rb12
-rw-r--r--app/uploaders/personal_file_uploader.rb70
-rw-r--r--app/views/admin/application_settings/_pages.html.haml5
-rw-r--r--app/views/admin/groups/_form.html.haml2
-rw-r--r--app/views/admin/health_check/show.html.haml2
-rw-r--r--app/views/admin/projects/_projects.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml8
-rw-r--r--app/views/admin/users/_access_levels.html.haml2
-rw-r--r--app/views/admin/users/_head.html.haml1
-rw-r--r--app/views/clusters/platforms/kubernetes/_form.html.haml8
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml15
-rw-r--r--app/views/devise/shared/_signup_box.html.haml1
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml1
-rw-r--r--app/views/events/_event.html.haml6
-rw-r--r--app/views/groups/settings/_general.html.haml20
-rw-r--r--app/views/help/index.html.haml3
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/_mailer.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/devise.html.haml3
-rw-r--r--app/views/layouts/header/_default.html.haml5
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml1
-rw-r--r--app/views/layouts/mailer.text.erb1
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml1
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml9
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml3
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml6
-rw-r--r--app/views/layouts/notify.html.haml2
-rw-r--r--app/views/layouts/notify.text.erb1
-rw-r--r--app/views/notify/closed_issue_email.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.text.haml2
-rw-r--r--app/views/projects/_files.html.haml10
-rw-r--r--app/views/projects/_flash_messages.html.haml1
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml3
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml3
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml2
-rw-r--r--app/views/projects/settings/_general.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/users/calendar_activities.html.haml2
-rw-r--r--app/views/users/show.html.haml6
-rw-r--r--app/workers/cluster_configure_worker.rb2
-rw-r--r--app/workers/pages_domain_removal_cron_worker.rb2
-rw-r--r--app/workers/process_commit_worker.rb2
-rw-r--r--changelogs/unreleased/1340-request-job-with-age.yml5
-rw-r--r--changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml5
-rw-r--r--changelogs/unreleased/237-style-toast-component.yml5
-rw-r--r--changelogs/unreleased/49517-fix-notes-import-export.yml5
-rw-r--r--changelogs/unreleased/49915-fix-error-500-admin-projects-nil-storage.yml5
-rw-r--r--changelogs/unreleased/60379-remove-ci-preparing-state-feature-flag.yml5
-rw-r--r--changelogs/unreleased/60818_yamllint_project_root.yml5
-rw-r--r--changelogs/unreleased/61606-support-string-piwik-website-ids.yml5
-rw-r--r--changelogs/unreleased/61629-dependency-installation-error-on-fsevents-1-2-4-with-node-js-12.yml5
-rw-r--r--changelogs/unreleased/61795-fix-error-when-moving-issues.yml5
-rw-r--r--changelogs/unreleased/61914-fix-emojis-urls.yml5
-rw-r--r--changelogs/unreleased/add-branch-to-project-search-api.yml5
-rw-r--r--changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml5
-rw-r--r--changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml5
-rw-r--r--changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml5
-rw-r--r--changelogs/unreleased/diff-whitespace-setting-changes.yml5
-rw-r--r--changelogs/unreleased/display-junit-classname-in-modal.yml5
-rw-r--r--changelogs/unreleased/dz-patch-58.yml5
-rw-r--r--changelogs/unreleased/fix-db-migrate-is-failed-on-mysql8.yml5
-rw-r--r--changelogs/unreleased/fix-project-visibility-level-validation.yml5
-rw-r--r--changelogs/unreleased/fix-too-many-loops-cron-error.yml5
-rw-r--r--changelogs/unreleased/fl-fix-next-flag-for-good.yml5
-rw-r--r--changelogs/unreleased/include-ee-fixtures.yml5
-rw-r--r--changelogs/unreleased/jc-omit-count-diverging-commits-max.yml5
-rw-r--r--changelogs/unreleased/kinolaev-master-patch-13154.yml5
-rw-r--r--changelogs/unreleased/leipert-node-12-compatibility.yml5
-rw-r--r--changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml5
-rw-r--r--changelogs/unreleased/mm12935.yml5
-rw-r--r--changelogs/unreleased/sh-fix-personal-snippet-uploads-object-storage.yml5
-rw-r--r--changelogs/unreleased/sh-fix-rebase-error-clearing.yml5
-rw-r--r--changelogs/unreleased/sh-fix-tag-push-remote-mirror.yml5
-rw-r--r--changelogs/unreleased/sh-update-process-mem.yml5
-rw-r--r--changelogs/unreleased/update-gitaly-to-v1-42-1.yml5
-rw-r--r--changelogs/unreleased/zj-usage-ping-pool-repository.yml5
-rw-r--r--config/environments/test.rb2
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/action_dispatch_http_mime_negotiation.rb2
-rw-r--r--config/initializers/seed_fu.rb4
-rw-r--r--config/karma.config.js2
-rw-r--r--config/puma.rb.example70
-rw-r--r--config/routes/admin.rb1
-rw-r--r--config/unicorn.rb.example.development1
-rw-r--r--db/migrate/20150509180749_convert_legacy_reference_notes.rb3
-rw-r--r--db/migrate/20170330141723_disable_invalid_service_templates2.rb2
-rw-r--r--db/migrate/20190515125613_add_application_settings_elasticsearch_shards.rb10
-rw-r--r--db/migrate/20190516011213_add_build_queued_at_index.rb19
-rw-r--r--db/post_migrate/20190301081611_migrate_project_migrate_sidekiq_queue.rb2
-rw-r--r--db/post_migrate/20190424134256_drop_projects_ci_id.rb9
-rw-r--r--db/schema.rb5
-rw-r--r--doc/README.md115
-rw-r--r--doc/administration/dependency_proxy.md150
-rw-r--r--doc/administration/geo/disaster_recovery/background_verification.md36
-rw-r--r--doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.pngbin0 -> 202130 bytes
-rw-r--r--doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.pngbin0 -> 85997 bytes
-rw-r--r--doc/administration/high_availability/nfs.md5
-rw-r--r--doc/administration/high_availability/nfs_host_client_setup.md2
-rw-r--r--doc/administration/img/high_availability/fully-distributed.pngbin46918 -> 0 bytes
-rw-r--r--doc/administration/img/high_availability/geo-ha-diagram.pngbin43826 -> 0 bytes
-rw-r--r--doc/administration/img/high_availability/horizontal.pngbin18660 -> 0 bytes
-rw-r--r--doc/administration/img/high_availability/hybrid.pngbin20698 -> 0 bytes
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md13
-rw-r--r--doc/api/epic_issues.md412
-rw-r--r--doc/api/epic_links.md252
-rw-r--r--doc/api/epics.md360
-rw-r--r--doc/api/geo_nodes.md408
-rw-r--r--doc/api/issue_links.md215
-rw-r--r--doc/api/issues.md66
-rw-r--r--doc/api/issues_statistics.md177
-rw-r--r--doc/api/license.md176
-rw-r--r--doc/api/license_templates.md5
-rw-r--r--doc/api/managed_licenses.md136
-rw-r--r--doc/api/merge_request_approvals.md425
-rw-r--r--doc/api/packages.md152
-rw-r--r--doc/api/repository_files.md2
-rw-r--r--doc/api/scim.md235
-rw-r--r--doc/api/search.md5
-rw-r--r--doc/api/vulnerabilities.md113
-rw-r--r--doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.pngbin9613 -> 0 bytes
-rw-r--r--doc/ci/environments.md4
-rw-r--r--doc/ci/examples/README.md48
-rw-r--r--doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md6
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.pngbin177704 -> 0 bytes
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md9
-rw-r--r--doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/img/job-succeeded.pngbin23148 -> 0 bytes
-rw-r--r--doc/ci/introduction/index.md2
-rw-r--r--doc/ci/merge_request_pipelines/img/pipeline_detail.pngbin15556 -> 0 bytes
-rw-r--r--doc/ci/merge_request_pipelines/index.md2
-rw-r--r--doc/ci/runners/shared_to_specific_admin.pngbin5715 -> 0 bytes
-rw-r--r--doc/ci/services/mysql.md27
-rw-r--r--doc/ci/yaml/README.md8
-rw-r--r--doc/development/README.md6
-rw-r--r--doc/development/architecture.md608
-rw-r--r--doc/development/documentation/index.md55
-rw-r--r--doc/development/ee_features.md24
-rw-r--r--doc/development/fe_guide/graphql.md8
-rw-r--r--doc/development/fe_guide/vuex.md4
-rw-r--r--doc/development/feature_flags.md8
-rw-r--r--doc/development/gitlab_architecture_diagram.pngbin34253 -> 0 bytes
-rw-r--r--doc/development/go_guide/index.md4
-rw-r--r--doc/development/rolling_out_changes_using_feature_flags.md11
-rw-r--r--doc/development/session.md65
-rw-r--r--doc/development/testing_guide/best_practices.md30
-rw-r--r--doc/development/testing_guide/review_apps.md22
-rw-r--r--doc/development/ux_guide/img/animation-autoscroll.gifbin302217 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/animation-dropdown.gifbin22483 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/animation-hover.gifbin247388 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/animation-quickupdate.gifbin6441 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/animation-reorder.gifbin70515 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-close--active.pngbin1120 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-close--hover.pngbin765 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-close--resting.pngbin915 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-danger--active.pngbin1117 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-danger--hover.pngbin797 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-danger--resting.pngbin932 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-info--active.pngbin1109 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-info--hover.pngbin795 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-info--resting.pngbin930 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-primary.pngbin1550 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-secondary.pngbin2683 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-spam--active.pngbin1107 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-spam--hover.pngbin777 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-spam--resting.pngbin941 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-success--active.pngbin1153 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-success--hover.pngbin892 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-success--resting.pngbin1045 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-success-secondary--active.pngbin1107 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-success-secondary--hover.pngbin774 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-success-secondary--resting.pngbin945 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-warning--active.pngbin1124 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-warning--hover.pngbin790 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/button-warning--resting.pngbin925 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/color-blue.pngbin1751 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/color-green.pngbin1916 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/color-grey.pngbin1681 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/color-orange.pngbin2094 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/color-red.pngbin1739 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/color-textprimary.pngbin2553 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/color-textsecondary.pngbin2956 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-alerts.pngbin27342 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-anchorlinks.pngbin16457 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-contentblock.pngbin14190 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-counts.pngbin2438 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-coverblock.pngbin10110 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-dateexact.pngbin4157 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-daterelative.pngbin4189 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-dropdown.pngbin31760 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-fileholder.pngbin3884 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-horizontalform.pngbin4310 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-listinsidepanel.pngbin3380 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-listwithavatar.pngbin5749 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-listwithhover.pngbin2722 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-panels.pngbin21822 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencehover.pngbin6890 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-referenceissues.pngbin10002 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencelabels.pngbin4099 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencemilestone.pngbin2412 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencemrs.pngbin8857 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-referencepeople.pngbin5600 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-rowcontentblock.pngbin14315 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-searchbox.pngbin3596 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-searchboxscoped.pngbin6027 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-simplelist.pngbin2776 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-table.pngbin6081 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/components-verticalform.pngbin4964 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-default.pngbin567 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-ibeam.pngbin383 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-move.pngbin276 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-panclosed.pngbin483 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-panopened.pngbin622 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/cursors-pointer.pngbin574 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/features-contextualnav.pngbin5911 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/features-emptystates.pngbin61644 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/features-filters.pngbin3924 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/features-globalnav.pngbin5780 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/harry-robison.pngbin10712 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/icon-add.pngbin239 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/icon-close.pngbin301 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/icon-edit.pngbin315 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/icon-notification.pngbin379 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/icon-rss.pngbin528 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/icon-spec.pngbin7523 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/icon-subscribe.pngbin537 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/icon-trash.pngbin281 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustration-size-large-horizontal.pngbin55272 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustration-size-large-vertical.pngbin59217 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustration-size-medium.pngbin20994 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustration-size-small.pngbin43536 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-border-radius.pngbin7779 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-caps-do.pngbin3671 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-caps-don't.pngbin3624 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-color-grey.pngbin251 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-color-orange.pngbin275 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-color-purple.pngbin275 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-geometric.pngbin5057 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-palette-oragne.pngbin10439 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/illustrations-palette-purple.pngbin10002 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/james-mackey.pngbin11869 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/karolina-plaskaty.pngbin10355 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/matthieu-poirier.pngbin11483 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/modals-general-confimation-dialog.pngbin15650 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/modals-layout-for-modals.pngbin20060 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/modals-special-confimation-dialog.pngbin31419 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/modals-three-buttons.pngbin17457 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/monospacefont-sample.pngbin14282 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/nazim-ramesh.pngbin10488 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/popover-placement-above.pngbin20184 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/popover-placement-below.pngbin19967 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/skeleton-loading.gifbin1093917 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/sourcesanspro-sample.pngbin10948 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/steven-lyons.pngbin9323 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-contentitemtitle.pngbin5139 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-header.pngbin4095 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-systeminformationblock.pngbin10422 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/surfaces-ux.pngbin4017 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/tooltip-placement.pngbin2066 -> 0 bytes
-rw-r--r--doc/development/ux_guide/img/tooltip-usage.pngbin5985 -> 0 bytes
-rw-r--r--doc/gitlab-basics/README.md6
-rw-r--r--doc/gitlab-basics/command-line-commands.md4
-rw-r--r--doc/gitlab-basics/img/new_issue_button.pngbin2010 -> 0 bytes
-rw-r--r--doc/gitlab-basics/img/new_issue_page.pngbin21386 -> 0 bytes
-rw-r--r--doc/gitlab-basics/img/public_file_link.pngbin3023 -> 0 bytes
-rw-r--r--doc/gitlab-basics/start-using-git.md24
-rw-r--r--doc/install/aws/img/add_tags.pngbin17834 -> 0 bytes
-rw-r--r--doc/install/aws/img/create_route_table.pngbin8293 -> 0 bytes
-rw-r--r--doc/install/azure/img/azure-vm-management-settings-network-interfaces.pngbin35849 -> 0 bytes
-rw-r--r--doc/install/azure/img/azure-vm-management.pngbin35363 -> 0 bytes
-rw-r--r--doc/install/installation.md78
-rw-r--r--doc/install/ldap.md4
-rw-r--r--doc/install/openshift_and_gitlab/img/pods-overview.pngbin42617 -> 0 bytes
-rw-r--r--doc/install/openshift_and_gitlab/img/storage-volumes.pngbin20007 -> 0 bytes
-rw-r--r--doc/integration/elasticsearch.md4
-rw-r--r--doc/integration/img/google_app.pngbin18853 -> 0 bytes
-rw-r--r--doc/integration/salesforce.md8
-rw-r--r--doc/license/README.md2
-rw-r--r--doc/monitoring/performance/img/grafana_dashboard_dropdown.pngbin7761 -> 0 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_dashboard_import.pngbin11835 -> 0 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_data_source_configuration.pngbin14695 -> 0 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_data_source_empty.pngbin11960 -> 0 bytes
-rw-r--r--doc/monitoring/performance/img/grafana_save_icon.pngbin4598 -> 0 bytes
-rw-r--r--doc/monitoring/performance/img/metrics_gitlab_configuration_settings.pngbin21382 -> 0 bytes
-rw-r--r--doc/topics/authentication/index.md10
-rw-r--r--doc/topics/autodevops/img/autodevops_domain_variables.pngbin8456 -> 0 bytes
-rw-r--r--doc/topics/autodevops/img/guide_connect_cluster.pngbin15225 -> 0 bytes
-rw-r--r--doc/topics/autodevops/img/guide_create_cluster.pngbin18915 -> 0 bytes
-rw-r--r--doc/topics/autodevops/img/guide_gke_apis_after.pngbin26811 -> 0 bytes
-rw-r--r--doc/topics/autodevops/img/guide_gke_apis_before.pngbin14882 -> 0 bytes
-rw-r--r--doc/topics/autodevops/img/guide_merge_request_ide.pngbin35052 -> 0 bytes
-rw-r--r--doc/topics/autodevops/index.md5
-rw-r--r--doc/university/high-availability/aws/img/elastic-file-system.pngbin34582 -> 0 bytes
-rw-r--r--doc/update/patch_versions.md2
-rw-r--r--doc/update/upgrading_from_source.md16
-rw-r--r--doc/user/admin_area/img/license_no_license_message.pngbin5778 -> 0 bytes
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md10
-rw-r--r--doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.pngbin5360 -> 0 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_group_edit.pngbin5869 -> 0 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_groups.pngbin12088 -> 0 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.pngbin4771 -> 0 bytes
-rw-r--r--doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.pngbin6000 -> 0 bytes
-rw-r--r--doc/user/admin_area/settings/img/group_quota_view.pngbin1797 -> 0 bytes
-rw-r--r--doc/user/admin_area/settings/img/group_settings.pngbin3345 -> 0 bytes
-rw-r--r--doc/user/application_security/container_scanning/index.md6
-rw-r--r--doc/user/application_security/dast/index.md2
-rw-r--r--doc/user/group/custom_project_templates.md2
-rw-r--r--doc/user/group/dependency_proxy/img/group_dependency_proxy.pngbin0 -> 40162 bytes
-rw-r--r--doc/user/group/dependency_proxy/index.md74
-rw-r--r--doc/user/group/img/group_issue_board.pngbin63528 -> 0 bytes
-rw-r--r--doc/user/group/img/new_group_form.pngbin32510 -> 0 bytes
-rw-r--r--doc/user/group/index.md4
-rw-r--r--doc/user/group/saml_sso/index.md12
-rw-r--r--doc/user/img/mermaid_diagram_render_gfm.pngbin2202 -> 0 bytes
-rw-r--r--doc/user/markdown.md23
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/clusters/index.md11
-rw-r--r--doc/user/project/code_owners.md2
-rw-r--r--doc/user/project/img/assignee_lists.pngbin134635 -> 0 bytes
-rw-r--r--doc/user/project/img/issue_board.pngbin284759 -> 0 bytes
-rw-r--r--doc/user/project/img/labels_sidebar_inline.pngbin11083 -> 0 bytes
-rw-r--r--doc/user/project/img/priority_sort_order.pngbin69978 -> 0 bytes
-rw-r--r--doc/user/project/img/project_security_dashboard.pngbin49062 -> 0 bytes
-rw-r--r--doc/user/project/img/protected_branches_choose_branch.pngbin7009 -> 0 bytes
-rw-r--r--doc/user/project/img/protected_branches_error_ui.pngbin13117 -> 0 bytes
-rw-r--r--doc/user/project/import/img/import_projects_from_github_select_auth_method.pngbin17611 -> 0 bytes
-rw-r--r--doc/user/project/integrations/img/jira_project_name.pngbin26680 -> 0 bytes
-rw-r--r--doc/user/project/integrations/img/jira_service.pngbin36976 -> 0 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_yaml_deploy.pngbin9456 -> 0 bytes
-rw-r--r--doc/user/project/integrations/webhooks.md20
-rw-r--r--doc/user/project/issues/csv_import.md20
-rw-r--r--doc/user/project/issues/img/group_issues_list_view.pngbin46595 -> 0 bytes
-rw-r--r--doc/user/project/issues/img/issue_template.pngbin25019 -> 0 bytes
-rw-r--r--doc/user/project/labels.md2
-rw-r--r--doc/user/project/members/img/add_new_user_to_project_settings.pngbin11004 -> 0 bytes
-rw-r--r--doc/user/project/members/img/add_user_members_menu.pngbin28988 -> 0 bytes
-rw-r--r--doc/user/project/members/img/max_access_level.pngbin34710 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/container_scanning.pngbin32549 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/create-issue-with-list-hover.pngbin106954 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/dast_all.pngbin25844 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/dast_single.pngbin69353 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/dependency_scanning.pngbin16167 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/interactive_reports.pngbin23190 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/license_management.pngbin5184 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/license_management_decision.pngbin5981 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/license_management_pipeline_tab.pngbin12115 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/license_management_settings.pngbin13300 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_commit.pngbin95647 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_mr.pngbin104954 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/sast.pngbin24876 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/security_report.pngbin38475 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/img/vulnerability_solution.pngbin3421 -> 0 bytes
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md17
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md4
-rw-r--r--doc/user/project/pages/img/pages_create_project.pngbin6062 -> 0 bytes
-rw-r--r--doc/user/project/pages/img/pages_create_user_page.pngbin14435 -> 0 bytes
-rw-r--r--doc/user/project/pages/img/pages_dns_details.pngbin5350 -> 0 bytes
-rw-r--r--doc/user/project/pages/img/pages_multiple_domains.pngbin12930 -> 0 bytes
-rw-r--r--doc/user/project/pages/img/pages_new_domain_button.pngbin8763 -> 0 bytes
-rw-r--r--doc/user/project/pages/img/pages_upload_cert.pngbin22888 -> 0 bytes
-rw-r--r--doc/user/project/web_ide/img/enable_web_ide.pngbin11364 -> 0 bytes
-rw-r--r--doc/user/project/wiki/index.md10
-rw-r--r--doc/user/search/img/issues_any_assignee.pngbin90455 -> 0 bytes
-rw-r--r--doc/user/search/img/issues_author.pngbin55217 -> 0 bytes
-rw-r--r--doc/workflow/award_emoji.pngbin5268 -> 0 bytes
-rw-r--r--doc/workflow/img/new_branch_from_issue.pngbin33584 -> 0 bytes
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md5
-rw-r--r--doc/workflow/share_with_group.pngbin50450 -> 0 bytes
-rw-r--r--fixtures/emojis/index.json2
-rw-r--r--lib/api/entities.rb21
-rw-r--r--lib/api/helpers/issues_helpers.rb33
-rw-r--r--lib/api/issues.rb101
-rw-r--r--lib/api/runner.rb1
-rw-r--r--lib/api/search.rb3
-rw-r--r--lib/api/validations/check_assignees_count.rb32
-rw-r--r--lib/bitbucket_server/representation/repo.rb2
-rw-r--r--lib/gitlab/background_migration/fix_cross_project_label_links.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/limit/activity.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/limit/size.rb4
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/dotNET-Core.yml91
-rw-r--r--lib/gitlab/config/entry/validators.rb6
-rw-r--r--lib/gitlab/danger/helper.rb2
-rw-r--r--lib/gitlab/git/repository.rb2
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml2
-rw-r--r--lib/gitlab/lets_encrypt/challenge.rb17
-rw-r--r--lib/gitlab/lets_encrypt/client.rb74
-rw-r--r--lib/gitlab/lets_encrypt/order.rb23
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb31
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb32
-rw-r--r--lib/gitlab/metrics/system.rb23
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/routing.rb2
-rw-r--r--lib/gitlab/sentry.rb15
-rw-r--r--lib/gitlab/usage_data.rb11
-rw-r--r--lib/mattermost/session.rb2
-rwxr-xr-xlib/support/init.d/gitlab37
-rw-r--r--lib/support/init.d/gitlab.default.example3
-rw-r--r--lib/system_check/base_check.rb2
-rw-r--r--lib/tasks/gitlab/features.rake2
-rw-r--r--lib/tasks/lint.rake35
-rw-r--r--locale/gitlab.pot73
-rw-r--r--package.json6
-rw-r--r--public/visual-review-toolbar.js626
-rw-r--r--qa/README.md6
-rw-r--r--qa/docs/BEST_PRACTICES.md38
-rw-r--r--qa/docs/GUIDELINES.md (renamed from qa/STYLE_GUIDE.md)2
-rw-r--r--qa/docs/WRITING_TESTS_FROM_SCRATCH.md380
-rw-r--r--qa/qa/page/base.rb8
-rw-r--r--qa/qa/page/project/branches/show.rb16
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb6
-rw-r--r--qa/qa/support/page/logging.rb22
-rw-r--r--qa/qa/support/waiter.rb4
-rw-r--r--qa/spec/page/logging_spec.rb9
-rw-r--r--qa/spec/spec_helper.rb8
-rwxr-xr-xscripts/clean-old-cached-assets4
-rw-r--r--scripts/frontend/stylelint/stylelint-utility-map.js2
-rw-r--r--spec/controllers/concerns/project_unauthorized_spec.rb2
-rw-r--r--spec/controllers/concerns/routable_actions_spec.rb156
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb3
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb22
-rw-r--r--spec/controllers/registrations_controller_spec.rb19
-rw-r--r--spec/factories/uploads.rb2
-rw-r--r--spec/features/admin/admin_sees_project_statistics_spec.rb29
-rw-r--r--spec/features/admin/admin_sees_projects_statistics_spec.rb20
-rw-r--r--spec/features/projects/files/files_sort_submodules_with_folders_spec.rb2
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb1
-rw-r--r--spec/features/projects/files/user_browses_lfs_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_creates_directory_spec.rb2
-rw-r--r--spec/features/projects/files/user_deletes_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb1
-rw-r--r--spec/features/projects/files/user_replaces_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb2
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb1
-rw-r--r--spec/features/projects_spec.rb1
-rw-r--r--spec/fixtures/api/schemas/entities/test_case.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/label_basic.json24
-rw-r--r--spec/frontend/boards/stores/actions_spec.js67
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js91
-rw-r--r--spec/frontend/boards/stores/state_spec.js11
-rw-r--r--spec/frontend/environment.js10
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js3
-rw-r--r--spec/frontend/helpers/timeout.js27
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js3
-rw-r--r--spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js3
-rw-r--r--spec/frontend/reports/components/report_item_spec.js33
-rw-r--r--spec/frontend/repository/router_spec.js23
-rw-r--r--spec/frontend/serverless/components/function_row_spec.js29
-rw-r--r--spec/frontend/serverless/components/functions_spec.js9
-rw-r--r--spec/frontend/serverless/components/missing_prometheus_spec.js15
-rw-r--r--spec/frontend/serverless/components/url_spec.js8
-rw-r--r--spec/frontend/test_setup.js11
-rw-r--r--spec/helpers/dashboard_helper_spec.rb6
-rw-r--r--spec/helpers/emails_helper_spec.rb39
-rw-r--r--spec/helpers/nav_helper_spec.rb12
-rw-r--r--spec/helpers/projects_helper_spec.rb20
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js30
-rw-r--r--spec/javascripts/fly_out_nav_spec.js5
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js2
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb7
-rw-r--r--spec/lib/gitlab/favicon_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb7
-rw-r--r--spec/lib/gitlab/lets_encrypt/challenge_spec.rb29
-rw-r--r--spec/lib/gitlab/lets_encrypt/client_spec.rb120
-rw-r--r--spec/lib/gitlab/lets_encrypt/order_spec.rb39
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb30
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb25
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb24
-rw-r--r--spec/lib/gitlab/sentry_spec.rb5
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb19
-rw-r--r--spec/models/active_session_spec.rb46
-rw-r--r--spec/models/application_record_spec.rb6
-rw-r--r--spec/models/ci/build_spec.rb20
-rw-r--r--spec/models/event_spec.rb2
-rw-r--r--spec/models/project_spec.rb7
-rw-r--r--spec/models/push_event_spec.rb4
-rw-r--r--spec/models/repository_spec.rb37
-rw-r--r--spec/requests/api/discussions_spec.rb4
-rw-r--r--spec/requests/api/helpers_spec.rb5
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb652
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb805
-rw-r--r--spec/requests/api/issues/issues_spec.rb796
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb549
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb392
-rw-r--r--spec/requests/api/issues_spec.rb2265
-rw-r--r--spec/requests/api/runner_spec.rb24
-rw-r--r--spec/requests/api/search_spec.rb7
-rw-r--r--spec/serializers/test_case_entity_spec.rb2
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb2
-rw-r--r--spec/services/clusters/refresh_service_spec.rb26
-rw-r--r--spec/services/git/base_hooks_service_spec.rb90
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb6
-rw-r--r--spec/services/git/branch_push_service_spec.rb66
-rw-r--r--spec/services/git/tag_hooks_service_spec.rb6
-rw-r--r--spec/services/issues/close_service_spec.rb44
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb26
-rw-r--r--spec/services/projects/create_service_spec.rb27
-rw-r--r--spec/services/projects/transfer_service_spec.rb27
-rw-r--r--spec/support/helpers/features/notes_helpers.rb10
-rw-r--r--spec/support/helpers/graphql_helpers.rb4
-rw-r--r--spec/support/helpers/lets_encrypt_helpers.rb19
-rw-r--r--spec/support/helpers/test_env.rb14
-rw-r--r--spec/support/shared_examples/requests/api/discussions.rb4
-rw-r--r--spec/support/shared_examples/requests/api/issues_shared_example_spec.rb44
-rw-r--r--spec/uploaders/object_storage_spec.rb2
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb60
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/workers/cluster_configure_worker_spec.rb21
-rw-r--r--spec/workers/pages_domain_removal_cron_worker_spec.rb15
-rw-r--r--yarn.lock35
622 files changed, 11873 insertions, 3761 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 9998ddba643..2be8e63e842 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -6,35 +6,35 @@ engines:
enabled: true
config:
languages:
- - ruby
- - javascript
+ - ruby
+ - javascript
ratings:
paths:
- - Gemfile.lock
- - "**.erb"
- - "**.haml"
- - "**.rb"
- - "**.rhtml"
- - "**.slim"
- - "**.inc"
- - "**.js"
- - "**.jsx"
- - "**.module"
+ - Gemfile.lock
+ - "**.erb"
+ - "**.haml"
+ - "**.rb"
+ - "**.rhtml"
+ - "**.slim"
+ - "**.inc"
+ - "**.js"
+ - "**.jsx"
+ - "**.module"
exclude_paths:
-- config/
-- db/
-- features/
-- node_modules/
-- spec/
-- vendor/
-- .yarn-cache/
-- tmp/
-- builds/
-- coverage/
-- public/
-- shared/
-- webpack-report/
-- log/
-- backups/
-- coverage-javascript/
-- plugins/
+ - config/
+ - db/
+ - features/
+ - node_modules/
+ - spec/
+ - vendor/
+ - .yarn-cache/
+ - tmp/
+ - builds/
+ - coverage/
+ - public/
+ - shared/
+ - webpack-report/
+ - log/
+ - backups/
+ - coverage-javascript/
+ - plugins/
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 3a024c44fe2..80356fa1dc2 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -78,7 +78,6 @@ schedule:review-build-cng:
<<: *review-base
stage: review
retry: 2
- allow_failure: true
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
@@ -130,7 +129,6 @@ review-stop:
.review-qa-base: &review-qa-base
<<: *review-docker
stage: qa
- allow_failure: true
variables:
<<: *review-docker-variables
QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
@@ -165,13 +163,14 @@ review-qa-smoke:
review-qa-all:
<<: *review-qa-base
+ allow_failure: true
when: manual
script:
- gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
.review-performance-base: &review-performance-base
<<: *review-qa-base
- stage: qa
+ allow_failure: true
before_script:
- export CI_ENVIRONMENT_URL="$(cat review_app_url.txt)"
- echo "${CI_ENVIRONMENT_URL}"
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index 3e58d2a867e..3adea22b33a 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -27,9 +27,9 @@ and verify the issue you're about to submit isn't a duplicate.
### Example Project
-(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report)
+(If possible, please create an example project here on GitLab.com that exhibits the problematic behavior, and link to it here in the bug report)
-(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version)
+(If you are using an older version of GitLab, this will also determine whether the bug is fixed in a more recent version)
### What is the current *bug* behavior?
@@ -42,7 +42,7 @@ and verify the issue you're about to submit isn't a duplicate.
### Relevant logs and/or screenshots
(Paste any relevant logs - please use code blocks (```) to format console output,
-logs, and code as it's very hard to read otherwise.)
+logs, and code as it's tough to read otherwise.)
### Output of checks
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 9946651075f..7857afb66c2 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -17,10 +17,10 @@ Set the title to: `Description of the original issue`
#### Backports
-- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases, plus the current RC if between the 7th and 22nd of the month.
+- [ ] Once the MR is ready to be merged, create MRs targeting the last 3 releases, plus the current RC if between the 7th and 22nd of the month.
- [ ] At this point, it might be easy to squash the commits from the MR into one
- You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation]
- - [ ] Create each MR targetting the stable branch `X-Y-stable`, using the "Security Release" merge request template.
+ - [ ] Create each MR targeting the stable branch `X-Y-stable`, using the "Security Release" merge request template.
- Every merge request will have its own set of TODOs, so make sure to
complete those.
- [ ] Make sure all MRs have a link in the [links section](#links)
diff --git a/.haml-lint.yml b/.haml-lint.yml
index e9cc4a91a21..e356be30c45 100644
--- a/.haml-lint.yml
+++ b/.haml-lint.yml
@@ -153,7 +153,7 @@ linters:
Indentation:
enabled: true
- character: space # or tab
+ character: space # or tab
TagName:
enabled: true
diff --git a/.pkgr.yml b/.pkgr.yml
index 10bcd7bd4bd..2e741f41a9e 100644
--- a/.pkgr.yml
+++ b/.pkgr.yml
@@ -3,8 +3,8 @@ group: git
services:
- postgres
before_precompile: ./bin/pkgr_before_precompile.sh
-env:
- - SKIP_STORAGE_VALIDATION=true
+env:
+ - SKIP_STORAGE_VALIDATION=true
targets:
debian-7: &wheezy
build_dependencies:
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a50908ca3da..e640847f99c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.42.0
+1.42.1
diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
new file mode 100644
index 00000000000..9084fa2f716
--- /dev/null
+++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
@@ -0,0 +1 @@
+1.1.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 47da986f86f..deeb3d66ef0 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-9.1.0
+9.2.0
diff --git a/Gemfile b/Gemfile
index 19432758b34..0b9e5000c7c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -60,6 +60,8 @@ gem 'u2f', '~> 0.2.1'
# GitLab Pages
gem 'validates_hostname', '~> 1.0.6'
gem 'rubyzip', '~> 1.2.2', require: 'zip'
+# GitLab Pages letsencrypt support
+gem 'acme-client', '~> 2.0.2'
# Browser detection
gem 'browser', '~> 2.5'
@@ -167,7 +169,7 @@ gem 'redis-namespace', '~> 1.6.0'
gem 'gitlab-sidekiq-fetcher', '~> 0.4.0', require: 'sidekiq-reliable-fetch'
# Cron Parser
-gem 'fugit', '~> 1.1'
+gem 'fugit', '~> 1.2.1'
# HTTP requests
gem 'httparty', '~> 0.16.4'
@@ -351,14 +353,14 @@ group :development, :test do
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
- gem 'gitlab-styles', '~> 2.6', require: false
+ gem 'gitlab-styles', '~> 2.7', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines
- gem 'rubocop', '~> 0.68.1'
+ gem 'rubocop', '~> 0.69.0'
gem 'rubocop-performance', '~> 1.1.0'
gem 'rubocop-rspec', '~> 1.22.1'
gem 'scss_lint', '~> 0.56.0', require: false
- gem 'haml_lint', '~> 0.30.0', require: false
+ gem 'haml_lint', '~> 0.31.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
@@ -407,7 +409,6 @@ gem 'health_check', '~> 2.6.0'
# System information
gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
-gem 'sys-proctable', '~> 1.2'
# SSH host key support
gem 'net-ssh', '~> 5.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3dd1adb5f63..be722b89a40 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,6 +4,8 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
+ acme-client (2.0.2)
+ faraday (~> 0.9, >= 0.9.1)
actioncable (5.1.7)
actionpack (= 5.1.7)
nio4r (~> 2.0)
@@ -85,7 +87,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
bindata (2.4.3)
- binding_ninja (0.2.2)
+ binding_ninja (0.2.3)
binding_of_caller (0.8.0)
debug_inspector (>= 0.0.1)
bootsnap (1.4.1)
@@ -190,7 +192,7 @@ GEM
equalizer (0.0.11)
erubi (1.8.0)
escape_utils (1.2.1)
- et-orbi (1.1.7)
+ et-orbi (1.2.1)
tzinfo
eventmachine (1.2.7)
excon (0.62.0)
@@ -264,15 +266,15 @@ GEM
foreman (0.84.0)
thor (~> 0.19.1)
formatador (0.2.5)
- fugit (1.1.9)
- et-orbi (~> 1.1, >= 1.1.7)
+ fugit (1.2.1)
+ et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.1)
fuubar (2.2.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
gemojione (3.3.0)
json
- get_process_mem (0.2.0)
+ get_process_mem (0.2.3)
gettext (3.2.9)
locale (>= 2.0.5)
text (>= 1.3.0)
@@ -297,8 +299,8 @@ GEM
gitlab-markup (1.7.0)
gitlab-sidekiq-fetcher (0.4.0)
sidekiq (~> 5)
- gitlab-styles (2.6.2)
- rubocop (~> 0.68.1)
+ gitlab-styles (2.7.0)
+ rubocop (~> 0.69.0)
rubocop-gitlab-security (~> 0.1.0)
rubocop-performance (~> 1.1.0)
rubocop-rspec (~> 1.19)
@@ -358,7 +360,7 @@ GEM
haml (5.0.4)
temple (>= 0.8.0)
tilt
- haml_lint (0.30.0)
+ haml_lint (0.31.0)
haml (>= 4.0, < 5.1)
rainbow
rake (>= 10, < 13)
@@ -591,7 +593,7 @@ GEM
orm_adapter (0.5.0)
os (1.0.0)
parallel (1.17.0)
- parser (2.5.3.0)
+ parser (2.6.3.0)
ast (~> 2.4.0)
parslet (1.8.2)
peek (1.0.1)
@@ -766,8 +768,8 @@ GEM
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
- rspec-parameterized (0.4.1)
- binding_ninja (>= 0.2.1)
+ rspec-parameterized (0.4.2)
+ binding_ninja (>= 0.2.3)
parser
proc_to_ast
rspec (>= 2.13, < 4)
@@ -791,13 +793,13 @@ GEM
pg
rails
sqlite3
- rubocop (0.68.1)
+ rubocop (0.69.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
- parser (>= 2.5, != 2.5.1.1)
+ parser (>= 2.6)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
- unicode-display_width (>= 1.4.0, < 1.6)
+ unicode-display_width (>= 1.4.0, < 1.7)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
rubocop-performance (1.1.0)
@@ -812,7 +814,7 @@ GEM
ruby-progressbar (1.10.0)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
- ruby_parser (3.11.0)
+ ruby_parser (3.13.1)
sexp_processor (~> 4.9)
rubyntlm (0.6.2)
rubypants (0.2.0)
@@ -852,7 +854,7 @@ GEM
sentry-raven (2.9.0)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
- sexp_processor (4.11.0)
+ sexp_processor (4.12.0)
sham_rack (1.3.6)
rack
shoulda-matchers (3.1.2)
@@ -904,8 +906,6 @@ GEM
httpclient (>= 2.4)
sys-filesystem (1.1.6)
ffi
- sys-proctable (1.2.1)
- ffi
sysexits (1.2.0)
temple (0.8.0)
test-prof (0.2.5)
@@ -937,7 +937,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
- unicode-display_width (1.5.0)
+ unicode-display_width (1.6.0)
unicorn (5.4.1)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -945,13 +945,13 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
- unparser (0.4.2)
+ unparser (0.4.5)
abstract_type (~> 0.0.7)
adamantium (~> 0.2.0)
concord (~> 0.1.5)
diff-lcs (~> 1.3)
equalizer (~> 0.0.9)
- parser (>= 2.3.1.2, < 2.6)
+ parser (~> 2.6.3)
procto (~> 0.0.2)
validate_email (0.1.6)
activemodel (>= 3.0)
@@ -998,6 +998,7 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
+ acme-client (~> 2.0.2)
activerecord_sane_schema_dumper (= 1.0)
acts-as-taggable-on (~> 6.0)
addressable (~> 2.5.2)
@@ -1061,7 +1062,7 @@ DEPENDENCIES
fog-rackspace (~> 0.1.1)
font-awesome-rails (~> 4.7)
foreman (~> 0.84.0)
- fugit (~> 1.1)
+ fugit (~> 1.2.1)
fuubar (~> 2.2.0)
gemojione (~> 3.3)
gettext (~> 3.2.2)
@@ -1073,7 +1074,7 @@ DEPENDENCIES
gitlab-labkit (~> 0.2.0)
gitlab-markup (~> 1.7.0)
gitlab-sidekiq-fetcher (~> 0.4.0)
- gitlab-styles (~> 2.6)
+ gitlab-styles (~> 2.7)
gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.2)
google-api-client (~> 0.23)
@@ -1086,7 +1087,7 @@ DEPENDENCIES
graphiql-rails (~> 1.4.10)
graphql (~> 1.8.0)
grpc (~> 1.19.0)
- haml_lint (~> 0.30.0)
+ haml_lint (~> 0.31.0)
hamlit (~> 2.8.8)
hangouts-chat (~> 0.0.5)
hashie-forbidden_attributes
@@ -1181,7 +1182,7 @@ DEPENDENCIES
rspec-set (~> 0.1.3)
rspec_junit_formatter
rspec_profiling (~> 0.0.5)
- rubocop (~> 0.68.1)
+ rubocop (~> 0.69.0)
rubocop-performance (~> 1.1.0)
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
@@ -1211,7 +1212,6 @@ DEPENDENCIES
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.5.1)
sys-filesystem (~> 1.1.6)
- sys-proctable (~> 1.2)
test-prof (~> 0.2.5)
thin (~> 1.7.0)
timecop (~> 0.8.0)
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index 798114b4b0b..d0b7f3ff7a2 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -15,7 +15,7 @@ import { sprintf, __ } from '../../locale';
// </pre>
//
-// This is an arbitary number; Can be iterated upon when suitable.
+// This is an arbitrary number; Can be iterated upon when suitable.
const MAX_CHAR_LIMIT = 5000;
export default function renderMermaid($els) {
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
new file mode 100644
index 00000000000..da82b52330a
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -0,0 +1,65 @@
+const notImplemented = () => {
+ throw new Error('Not implemented!');
+};
+
+export default {
+ setEndpoints: () => {
+ notImplemented();
+ },
+
+ fetchLists: () => {
+ notImplemented();
+ },
+
+ generateDefaultLists: () => {
+ notImplemented();
+ },
+
+ createList: () => {
+ notImplemented();
+ },
+
+ updateList: () => {
+ notImplemented();
+ },
+
+ deleteList: () => {
+ notImplemented();
+ },
+
+ fetchIssuesForList: () => {
+ notImplemented();
+ },
+
+ moveIssue: () => {
+ notImplemented();
+ },
+
+ createNewIssue: () => {
+ notImplemented();
+ },
+
+ fetchBacklog: () => {
+ notImplemented();
+ },
+
+ bulkUpdateIssues: () => {
+ notImplemented();
+ },
+
+ fetchIssue: () => {
+ notImplemented();
+ },
+
+ toggleIssueSubscription: () => {
+ notImplemented();
+ },
+
+ showPage: () => {
+ notImplemented();
+ },
+
+ toggleEmptyState: () => {
+ notImplemented();
+ },
+};
diff --git a/app/assets/javascripts/boards/stores/index.js b/app/assets/javascripts/boards/stores/index.js
new file mode 100644
index 00000000000..f70395a3771
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/index.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from 'ee_else_ce/boards/stores/state';
+import actions from 'ee_else_ce/boards/stores/actions';
+import mutations from 'ee_else_ce/boards/stores/mutations';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ state,
+ actions,
+ mutations,
+ });
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
new file mode 100644
index 00000000000..fcdfa6799b6
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -0,0 +1,21 @@
+export const SET_ENDPOINTS = 'SET_ENDPOINTS';
+export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST';
+export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
+export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR';
+export const REQUEST_UPDATE_LIST = 'REQUEST_UPDATE_LIST';
+export const RECEIVE_UPDATE_LIST_SUCCESS = 'RECEIVE_UPDATE_LIST_SUCCESS';
+export const RECEIVE_UPDATE_LIST_ERROR = 'RECEIVE_UPDATE_LIST_ERROR';
+export const REQUEST_REMOVE_LIST = 'REQUEST_REMOVE_LIST';
+export const RECEIVE_REMOVE_LIST_SUCCESS = 'RECEIVE_REMOVE_LIST_SUCCESS';
+export const RECEIVE_REMOVE_LIST_ERROR = 'RECEIVE_REMOVE_LIST_ERROR';
+export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE';
+export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS';
+export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR';
+export const REQUEST_MOVE_ISSUE = 'REQUEST_MOVE_ISSUE';
+export const RECEIVE_MOVE_ISSUE_SUCCESS = 'RECEIVE_MOVE_ISSUE_SUCCESS';
+export const RECEIVE_MOVE_ISSUE_ERROR = 'RECEIVE_MOVE_ISSUE_ERROR';
+export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE';
+export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
+export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
+export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
+export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
new file mode 100644
index 00000000000..77ba68be07e
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -0,0 +1,91 @@
+import * as mutationTypes from './mutation_types';
+
+const notImplemented = () => {
+ throw new Error('Not implemented!');
+};
+
+export default {
+ [mutationTypes.SET_ENDPOINTS]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.REQUEST_ADD_LIST]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_ADD_LIST_ERROR]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.REQUEST_UPDATE_LIST]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_UPDATE_LIST_SUCCESS]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_UPDATE_LIST_ERROR]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.REQUEST_REMOVE_LIST]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_REMOVE_LIST_SUCCESS]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_REMOVE_LIST_ERROR]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.REQUEST_ADD_ISSUE]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_ADD_ISSUE_SUCCESS]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_ADD_ISSUE_ERROR]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.REQUEST_MOVE_ISSUE]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_MOVE_ISSUE_SUCCESS]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_MOVE_ISSUE_ERROR]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.REQUEST_UPDATE_ISSUE]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_UPDATE_ISSUE_SUCCESS]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.RECEIVE_UPDATE_ISSUE_ERROR]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.SET_CURRENT_PAGE]: () => {
+ notImplemented();
+ },
+
+ [mutationTypes.TOGGLE_EMPTY_STATE]: () => {
+ notImplemented();
+ },
+};
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
new file mode 100644
index 00000000000..dd16abb01a5
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -0,0 +1,3 @@
+export default () => ({
+ // ...
+});
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 03dea1ec0a5..b62ec8a651b 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -8,6 +8,8 @@ import { parseBoolean } from '~/lib/utils/common_utils';
// https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24555#note_134136110
const NAV_SIDEBAR_BREAKPOINT = 1200;
+export const SIDEBAR_COLLAPSED_CLASS = 'js-sidebar-collapsed';
+
export default class ContextualSidebar {
constructor() {
this.initDomElements();
@@ -62,6 +64,7 @@ export default class ContextualSidebar {
const breakpoint = bp.getBreakpointSize();
const dbp = ContextualSidebar.isDesktopBreakpoint();
+ this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? show : false);
this.$overlay.toggleClass(
'mobile-nav-open',
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 0ed4dcdcd81..11d6672cacf 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -157,10 +157,12 @@ export default {
this.adjustView();
eventHub.$once('fetchedNotesData', this.setDiscussions);
eventHub.$once('fetchDiffData', this.fetchData);
+ eventHub.$on('refetchDiffData', this.refetchDiffData);
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
},
beforeDestroy() {
eventHub.$off('fetchDiffData', this.fetchData);
+ eventHub.$off('refetchDiffData', this.refetchDiffData);
this.removeEventListeners();
},
methods: {
@@ -175,10 +177,16 @@ export default {
'scrollToFile',
'toggleShowTreeList',
]),
- fetchData() {
+ refetchDiffData() {
+ this.assignedDiscussions = false;
+ this.fetchData(false);
+ },
+ fetchData(toggleTree = true) {
this.fetchDiffFiles()
.then(() => {
- this.hideTreeListIfJustOneFile();
+ if (toggleTree) {
+ this.hideTreeListIfJustOneFile();
+ }
requestIdleCallback(
() => {
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 384f33e0983..30be2e68e76 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -1,6 +1,7 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue';
@@ -57,6 +58,9 @@ export default {
this.search = '';
},
},
+ searchPlaceholder: sprintf(s__('MergeRequest|Filter files or search with %{modifier_key}+p'), {
+ modifier_key: /Mac/i.test(navigator.userAgent) ? 'cmd' : 'ctrl',
+ }),
};
</script>
@@ -65,10 +69,13 @@ export default {
<div class="append-bottom-8 position-relative tree-list-search d-flex">
<div class="flex-fill d-flex">
<icon name="search" class="position-absolute tree-list-icon" />
+ <label for="diff-tree-search" class="sr-only">{{ $options.searchPlaceholder }}</label>
<input
+ id="diff-tree-search"
v-model="search"
- :placeholder="s__('MergeRequest|Filter files')"
+ :placeholder="$options.searchPlaceholder"
type="search"
+ name="diff-tree-search"
class="form-control"
/>
<button
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 386d08aed2b..35297b7c264 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -52,7 +52,7 @@ export const fetchDiffFiles = ({ state, commit }) => {
});
return axios
- .get(state.endpoint, { params: { w: state.showWhitespace ? null : '1' } })
+ .get(mergeUrlParams({ w: state.showWhitespace ? '0' : '1' }, state.endpoint))
.then(res => {
commit(types.SET_LOADING, false);
commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
@@ -125,7 +125,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
new Promise(resolve => {
const nextFile = state.diffFiles.find(
file =>
- !file.renderIt && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text),
+ !file.renderIt &&
+ (file.viewer && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text)),
);
if (nextFile) {
@@ -315,8 +316,10 @@ export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = fals
localStorage.setItem(WHITESPACE_STORAGE_KEY, showWhitespace);
if (pushState) {
- historyPushState(showWhitespace ? '?w=0' : '?w=1');
+ historyPushState(mergeUrlParams({ w: showWhitespace ? '0' : '1' }, window.location.href));
}
+
+ eventHub.$emit('refetchDiffData');
};
export const toggleFileFinder = ({ commit }, visible) => {
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 2b6af9060d1..2566ed6b47c 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -1,4 +1,5 @@
import bp from './breakpoints';
+import { SIDEBAR_COLLAPSED_CLASS } from './contextual_sidebar';
const HIDE_INTERVAL_TIMEOUT = 300;
const IS_OVER_CLASS = 'is-over';
@@ -29,7 +30,7 @@ const setHeaderHeight = () => {
};
export const isSidebarCollapsed = () =>
- sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
+ sidebar && sidebar.classList.contains(SIDEBAR_COLLAPSED_CLASS);
export const canShowActiveSubItems = el => {
if (el.classList.contains('active') && !isSidebarCollapsed()) {
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
index a5b8c357e8a..04301c9ce12 100644
--- a/app/assets/javascripts/gl_field_error.js
+++ b/app/assets/javascripts/gl_field_error.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { __ } from '~/locale';
/**
* This class overrides the browser's validation error bubbles, displaying custom
@@ -61,7 +62,7 @@ export default class GlFieldError {
this.inputElement = $(input);
this.inputDomElement = this.inputElement.get(0);
this.form = formErrors;
- this.errorMessage = this.inputElement.attr('title') || 'This field is required.';
+ this.errorMessage = this.inputElement.attr('title') || __('This field is required.');
this.fieldErrorElement = $(`<p class='${errorMessageClass} hidden'>${this.errorMessage}</p>`);
this.state = {
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index efba6fc1aff..96051b612b5 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -20,7 +20,7 @@ export default class GpgBadges {
const endpoint = tag.data('signaturesPath');
if (!endpoint) {
displayError();
- return Promise.reject(new Error('Missing commit signatures endpoint!'));
+ return Promise.reject(new Error(__('Missing commit signatures endpoint!')));
}
const params = parseQueryStringIntoObject(tag.serialize());
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index bdadbb1bb2a..a1263d1cdab 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import Api from './api';
import { normalizeHeaders } from './lib/utils/common_utils';
+import { __ } from '~/locale';
export default function groupsSelect() {
import(/* webpackChunkName: 'select2' */ 'select2/select2')
@@ -18,7 +19,7 @@ export default function groupsSelect() {
: Api.groupsPath;
$select.select2({
- placeholder: 'Search for a group',
+ placeholder: __('Search for a group'),
allowClear: $select.hasClass('allowClear'),
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index ccbe591a63e..bc9d7fcf30d 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import Flash from './flash';
+import { __ } from './locale';
export default {
init({ container, form, issues, prefixId } = {}) {
@@ -32,7 +33,7 @@ export default {
onFormSubmitFailure() {
this.form.find('[type="submit"]').enable();
- return new Flash('Issue update failed');
+ return new Flash(__('Issue update failed'));
},
getSelectedIssues() {
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index ffcbd7cf28c..f51c7a2f990 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import flash from './flash';
-import { __ } from './locale';
+import { s__, __ } from './locale';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
@@ -29,7 +29,7 @@ export default class IssuableIndex {
$resetToken.on('click', e => {
e.preventDefault();
- $resetToken.text('resetting...');
+ $resetToken.text(s__('EmailToken|resetting...'));
axios
.put($resetToken.attr('href'))
@@ -38,12 +38,12 @@ export default class IssuableIndex {
.val(data.new_address)
.focus();
- $resetToken.text('reset it');
+ $resetToken.text(s__('EmailToken|reset it'));
})
.catch(() => {
flash(__('There was an error when reseting email token.'));
- $resetToken.text('reset it');
+ $resetToken.text(s__('EmailToken|reset it'));
});
});
}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index cd1afb6ba83..db4607ca58d 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -7,6 +7,7 @@ import flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
+import { __ } from './locale';
export default class Issue {
constructor() {
@@ -44,7 +45,11 @@ export default class Issue {
* @param {Array} data
* @param {String} issueFailMessage
*/
- updateTopState(isClosed, data, issueFailMessage = 'Unable to update this issue at this time.') {
+ updateTopState(
+ isClosed,
+ data,
+ issueFailMessage = __('Unable to update this issue at this time.'),
+ ) {
if ('id' in data) {
const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
@@ -81,7 +86,7 @@ export default class Issue {
}
initIssueBtnEventListeners() {
- const issueFailMessage = 'Unable to update this issue at this time.';
+ const issueFailMessage = __('Unable to update this issue at this time.');
return $(document).on(
'click',
@@ -152,6 +157,6 @@ export default class Issue {
$container.html(data.html);
}
})
- .catch(() => flash('Failed to load related branches'));
+ .catch(() => flash(__('Failed to load related branches')));
}
}
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index c14803c80e7..75edff41a89 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { __ } from './locale';
export default function issueStatusSelect() {
$('.js-issue-status').each((i, el) => {
@@ -7,7 +8,7 @@ export default function issueStatusSelect() {
selectable: true,
fieldName,
toggleLabel(selected, element, instance) {
- let label = 'Author';
+ let label = __('Author');
const $item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js
index 4f7eff2cca1..8f0afa3467d 100644
--- a/app/assets/javascripts/lib/utils/highlight.js
+++ b/app/assets/javascripts/lib/utils/highlight.js
@@ -27,14 +27,14 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match
const sanitizedValue = sanitize(string.toString(), { allowedTags: [] });
- // occurences is an array of character indices that should be
+ // occurrences is an array of character indices that should be
// highlighted in the original string, i.e. [3, 4, 5, 7]
- const occurences = fuzzaldrinPlus.match(sanitizedValue, match.toString());
+ const occurrences = fuzzaldrinPlus.match(sanitizedValue, match.toString());
return sanitizedValue
.split('')
.map((character, i) => {
- if (_.contains(occurences, i)) {
+ if (_.contains(occurrences, i)) {
return `${matchPrefix}${character}${matchSuffix}`;
}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 213d5c6521a..9f30a989295 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -31,6 +31,7 @@ import initPerformanceBar from './performance_bar';
import initSearchAutocomplete from './search_autocomplete';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
+import { __ } from './locale';
// expose jQuery as global (TODO: remove these)
window.jQuery = jQuery;
@@ -147,7 +148,7 @@ function deferredInitialisation() {
const canaryBadge = document.querySelector('.js-canary-badge');
const canaryLink = document.querySelector('.js-canary-link');
if (canaryBadge) {
- canaryBadge.classList.add('hidden');
+ canaryBadge.classList.remove('hidden');
}
if (canaryLink) {
canaryLink.classList.add('hidden');
@@ -219,9 +220,9 @@ document.addEventListener('DOMContentLoaded', () => {
const ref = xhrObj.status;
if (ref === 401) {
- Flash('You need to be logged in.');
+ Flash(__('You need to be logged in.'));
} else if (ref === 404 || ref === 500) {
- Flash('Something went wrong on our end.');
+ Flash(__('Something went wrong on our end.'));
}
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 509f19e6f00..e5cf43e8289 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -21,6 +21,7 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
import Notes from './notes';
import { polyfillSticky } from './lib/utils/sticky';
+import { __ } from './locale';
// MergeRequestTabs
//
@@ -326,7 +327,7 @@ export default class MergeRequestTabs {
})
.catch(() => {
this.toggleLoading(false);
- flash('An error occurred while fetching this tab.');
+ flash(__('An error occurred while fetching this tab.'));
});
}
@@ -416,7 +417,7 @@ export default class MergeRequestTabs {
})
.catch(() => {
this.toggleLoading(false);
- flash('An error occurred while fetching this tab.');
+ flash(__('An error occurred while fetching this tab.'));
});
}
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index f211632cf24..6aaba4e7c74 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import flash from './flash';
import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
+import { __ } from './locale';
export default class Milestone {
constructor() {
@@ -42,7 +43,7 @@ export default class Milestone {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
})
- .catch(() => flash('Error loading milestone tab'));
+ .catch(() => flash(__('Error loading milestone tab')));
}
}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 75c18a9b6a0..43949d5cc86 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -56,14 +56,15 @@ export default class MilestoneSelect {
const $value = $block.find('.value');
const $loading = $block.find('.block-loading').fadeOut();
selectedMilestoneDefault = showAny ? '' : null;
- selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault;
+ selectedMilestoneDefault =
+ showNo && defaultNo ? __('No Milestone') : selectedMilestoneDefault;
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
milestoneLinkTemplate = _.template(
'<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
);
- milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
+ milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`;
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
@@ -74,28 +75,28 @@ export default class MilestoneSelect {
extraOptions.push({
id: null,
name: null,
- title: 'Any Milestone',
+ title: __('Any Milestone'),
});
}
if (showNo) {
extraOptions.push({
id: -1,
- name: 'No Milestone',
- title: 'No Milestone',
+ name: __('No Milestone'),
+ title: __('No Milestone'),
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
- title: 'Upcoming',
+ title: __('Upcoming'),
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
- title: 'Started',
+ title: __('Started'),
});
}
if (extraOptions.length) {
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index 81ab9d8be4b..b39ad764f01 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import flash from './flash';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
/**
* In each pipelines table we have a mini pipeline graph for each pipeline.
@@ -98,7 +99,7 @@ export default class MiniPipelineGraph {
) {
$(button).dropdown('toggle');
}
- flash('An error occurred while fetching the builds.', 'alert');
+ flash(__('An error occurred while fetching the builds.'), 'alert');
});
}
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index ee1a5274ff7..03d349ac714 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import Api from './api';
import { mergeUrlParams } from './lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { __ } from './locale';
export default class NamespaceSelect {
constructor(opts) {
@@ -29,7 +30,7 @@ export default class NamespaceSelect {
return Api.namespaces(term, function(namespaces) {
if (isFilter) {
const anyNamespace = {
- text: 'Any namespace',
+ text: __('Any namespace'),
id: null,
};
namespaces.unshift(anyNamespace);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 36725e22365..d03f4508fb8 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -35,6 +35,7 @@ import {
} from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
+import { sprintf, s__, __ } from './locale';
window.autosize = Autosize;
@@ -253,7 +254,7 @@ export default class Notes {
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) {
if ($textarea.val() !== '') {
- if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
+ if (!window.confirm(__('Are you sure you want to cancel creating this comment?'))) {
return;
}
}
@@ -265,7 +266,7 @@ export default class Notes {
originalText = $textarea.closest('form').data('originalNote');
newText = $textarea.val();
if (originalText !== newText) {
- if (!window.confirm('Are you sure you want to cancel editing this comment?')) {
+ if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) {
return;
}
}
@@ -636,7 +637,7 @@ export default class Notes {
this.glForm = new GLForm(form, enableGFM);
textarea = form.find('.js-note-text');
key = [
- 'Note',
+ s__('NoteForm|Note'),
form.find('#note_noteable_type').val(),
form.find('#note_noteable_id').val(),
form.find('#note_commit_id').val(),
@@ -670,7 +671,9 @@ export default class Notes {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
return this.addFlash(
- 'Your comment could not be submitted! Please check your network connection and try again.',
+ __(
+ 'Your comment could not be submitted! Please check your network connection and try again.',
+ ),
'alert',
formParentTimeline.get(0),
);
@@ -679,7 +682,7 @@ export default class Notes {
updateNoteError($parentTimeline) {
// eslint-disable-next-line no-new
new Flash(
- 'Your comment could not be updated! Please check your network connection and try again.',
+ __('Your comment could not be updated! Please check your network connection and try again.'),
);
}
@@ -1258,12 +1261,19 @@ export default class Notes {
putConflictEditWarningInPlace(noteEntity, $note) {
if ($note.find('.js-conflict-edit-warning').length === 0) {
+ const open_link = `<a href="#note_${
+ noteEntity.id
+ }" target="_blank" rel="noopener noreferrer">`;
const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
- This comment has changed since you started editing, please review the
- <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">
- updated comment
- </a>
- to ensure information is not lost
+ ${sprintf(
+ s__(
+ 'Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost',
+ ),
+ {
+ open_link,
+ close_link: '</a>',
+ },
+ )}
</div>`);
$alert.insertAfter($note.find('.note-text'));
}
@@ -1491,13 +1501,15 @@ export default class Notes {
if (executedCommands && executedCommands.length) {
if (executedCommands.length > 1) {
- tempFormContent = 'Applying multiple commands';
+ tempFormContent = __('Applying multiple commands');
} else {
const commandDescription = executedCommands[0].description.toLowerCase();
- tempFormContent = `Applying command to ${commandDescription}`;
+ tempFormContent = sprintf(__('Applying command to %{commandDescription}'), {
+ commandDescription,
+ });
}
} else {
- tempFormContent = 'Applying command';
+ tempFormContent = __('Applying command');
}
return tempFormContent;
@@ -1817,7 +1829,9 @@ export default class Notes {
$editingNote
.find('.note-headline-meta a')
.html(
- '<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>',
+ `<i class="fa fa-spinner fa-spin" aria-label="${__(
+ 'Comment is being updated',
+ )}" aria-hidden="true"></i>`,
);
// Make request to update comment on server
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 869f70e7d33..6aa41d0825b 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -46,4 +46,12 @@ document.addEventListener('DOMContentLoaded', () => {
GpgBadges.fetch();
leaveByUrl('project');
+
+ if (document.getElementById('js-tree-list')) {
+ import('~/repository')
+ .then(m => m.default())
+ .catch(e => {
+ throw e;
+ });
+ }
});
diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js
index 400aed35e32..7b90a3a4f6e 100644
--- a/app/assets/javascripts/pages/projects/tree/show/index.js
+++ b/app/assets/javascripts/pages/projects/tree/show/index.js
@@ -40,4 +40,12 @@ document.addEventListener('DOMContentLoaded', () => {
}
GpgBadges.fetch();
+
+ if (document.getElementById('js-tree-list')) {
+ import('~/repository')
+ .then(m => m.default())
+ .catch(e => {
+ throw e;
+ });
+ }
});
diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue
index f4243522ef8..50f2910e02d 100644
--- a/app/assets/javascripts/reports/components/issues_list.vue
+++ b/app/assets/javascripts/reports/components/issues_list.vue
@@ -52,6 +52,11 @@ export default {
required: false,
default: '',
},
+ showReportSectionStatus: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
issuesWithState() {
@@ -81,6 +86,7 @@ export default {
:status="wrapped.status"
:component="component"
:is-new="wrapped.isNew"
+ :show-report-section-status="showReportSectionStatus"
/>
</smart-virtual-list>
</template>
diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue
index d2106f9ad2e..01a30809e1a 100644
--- a/app/assets/javascripts/reports/components/report_item.vue
+++ b/app/assets/javascripts/reports/components/report_item.vue
@@ -34,12 +34,22 @@ export default {
required: false,
default: false,
},
+ showReportSectionStatusIcon: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
<template>
<li :class="{ 'is-dismissed': issue.isDismissed }" class="report-block-list-issue">
- <issue-status-icon :status="status" :status-icon-size="statusIconSize" class="append-right-5" />
+ <issue-status-icon
+ v-if="showReportSectionStatusIcon"
+ :status="status"
+ :status-icon-size="statusIconSize"
+ class="append-right-5"
+ />
<component :is="component" v-if="component" :issue="issue" :status="status" :is-new="isNew" />
</li>
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index d6483e95278..420e71f5e86 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -73,6 +73,11 @@ export default {
default: () => ({}),
required: false,
},
+ showReportSectionStatusIcon: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
@@ -166,6 +171,7 @@ export default {
:resolved-issues="resolvedIssues"
:neutral-issues="neutralIssues"
:component="component"
+ :show-report-section-status-icon="showReportSectionStatusIcon"
/>
</slot>
</div>
diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js
index 5484900276c..25f9f70d095 100644
--- a/app/assets/javascripts/reports/store/state.js
+++ b/app/assets/javascripts/reports/store/state.js
@@ -40,6 +40,11 @@ export default () => ({
text: s__('Reports|Class'),
type: fieldTypes.link,
},
+ classname: {
+ value: null,
+ text: s__('Reports|Classname'),
+ type: fieldTypes.text,
+ },
execution_time: {
value: null,
text: s__('Reports|Execution time'),
diff --git a/app/assets/javascripts/repository/components/app.vue b/app/assets/javascripts/repository/components/app.vue
new file mode 100644
index 00000000000..98240aef810
--- /dev/null
+++ b/app/assets/javascripts/repository/components/app.vue
@@ -0,0 +1,3 @@
+<template>
+ <router-view />
+</template>
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
new file mode 100644
index 00000000000..febfcce780c
--- /dev/null
+++ b/app/assets/javascripts/repository/graphql.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+const defaultClient = createDefaultClient({});
+
+export default new VueApollo({
+ defaultClient,
+});
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
new file mode 100644
index 00000000000..00b69362312
--- /dev/null
+++ b/app/assets/javascripts/repository/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import createRouter from './router';
+import App from './components/app.vue';
+import apolloProvider from './graphql';
+
+export default function setupVueRepositoryList() {
+ const el = document.getElementById('js-tree-list');
+ const { projectPath, ref } = el.dataset;
+
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ projectPath,
+ ref,
+ },
+ });
+
+ return new Vue({
+ el,
+ router: createRouter(projectPath, ref),
+ apolloProvider,
+ render(h) {
+ return h(App);
+ },
+ });
+}
diff --git a/app/assets/javascripts/repository/pages/index.vue b/app/assets/javascripts/repository/pages/index.vue
new file mode 100644
index 00000000000..fdbf195f0f6
--- /dev/null
+++ b/app/assets/javascripts/repository/pages/index.vue
@@ -0,0 +1,24 @@
+<script>
+import getRef from '../queries/getRef.graphql';
+
+export default {
+ apollo: {
+ ref: {
+ query: getRef,
+ },
+ },
+ data() {
+ return {
+ ref: '',
+ };
+ },
+};
+</script>
+
+<template>
+ <div>
+ <router-link :to="{ path: `/tree/${ref}/app` }">
+ Go to tree
+ </router-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/pages/tree.vue b/app/assets/javascripts/repository/pages/tree.vue
new file mode 100644
index 00000000000..f51aafee775
--- /dev/null
+++ b/app/assets/javascripts/repository/pages/tree.vue
@@ -0,0 +1,15 @@
+<script>
+export default {
+ props: {
+ path: {
+ type: String,
+ required: false,
+ default: '/',
+ },
+ },
+};
+</script>
+
+<template>
+ <div>{{ path }}</div>
+</template>
diff --git a/app/assets/javascripts/repository/queries/getRef.graphql b/app/assets/javascripts/repository/queries/getRef.graphql
new file mode 100644
index 00000000000..58c09844c3f
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getRef.graphql
@@ -0,0 +1,3 @@
+query getRef {
+ ref @client
+}
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
new file mode 100644
index 00000000000..b42a96a4ee2
--- /dev/null
+++ b/app/assets/javascripts/repository/router.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { joinPaths } from '../lib/utils/url_utility';
+import IndexPage from './pages/index.vue';
+import TreePage from './pages/tree.vue';
+
+Vue.use(VueRouter);
+
+export default function createRouter(base, baseRef) {
+ return new VueRouter({
+ mode: 'history',
+ base: joinPaths(gon.relative_url_root || '', base),
+ routes: [
+ {
+ path: '/',
+ name: 'projectRoot',
+ component: IndexPage,
+ },
+ {
+ path: `/tree/${baseRef}(/.*)?`,
+ name: 'treePath',
+ component: TreePage,
+ props: route => ({
+ path: route.params.pathMatch,
+ }),
+ beforeEnter(to, from, next) {
+ document
+ .querySelectorAll('.js-hide-on-navigation')
+ .forEach(el => el.classList.add('hidden'));
+
+ next();
+ },
+ },
+ ],
+ });
+}
diff --git a/app/assets/stylesheets/components/toast.scss b/app/assets/stylesheets/components/toast.scss
index 91d16c8e98d..33e1c4e5349 100644
--- a/app/assets/stylesheets/components/toast.scss
+++ b/app/assets/stylesheets/components/toast.scss
@@ -1,3 +1,53 @@
-.toast-close {
- font-size: $default-icon-size !important;
+/*
+* These styles are specific to the gl-toast component.
+* Documentation: https://design.gitlab.com/components/toasts
+* Note: Styles below are nested in order to override some of vue-toasted's default styling
+*/
+.toasted-container {
+
+ max-width: $toast-max-width;
+
+ @include media-breakpoint-down(xs) {
+ width: 100%;
+ padding-right: $toast-padding-right;
+ }
+
+ .toasted.gl-toast {
+ border-radius: $border-radius-default;
+ font-size: $gl-font-size;
+ padding: $gl-padding-8 $gl-padding-24;
+ margin-top: $toast-default-margin;
+ line-height: $gl-line-height;
+ background-color: rgba($gray-900, $toast-background-opacity);
+
+ @include media-breakpoint-down(xs) {
+ .action:first-child {
+ // Ensures actions buttons are right aligned on mobile
+ margin-left: auto;
+ }
+ }
+
+ .action {
+ color: $blue-300;
+ margin: 0 0 0 $toast-action-margin-left;
+ text-transform: none;
+ font-size: $gl-font-size;
+
+ &:first-child {
+ padding-right: 0;
+ }
+ }
+
+ .toast-close {
+ font-size: $default-icon-size;
+ margin-left: $toast-default-margin;
+ padding-left: $gl-padding;
+ }
+ }
+}
+
+// Overrides the default positioning of toasts
+body .toasted-container.bottom-left {
+ bottom: $toast-offset;
+ left: $toast-offset;
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index e6c55252b24..3aabb66f7a6 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -22,6 +22,10 @@
}
}
+.oneline {
+ line-height: 35px;
+}
+
.row-content-block {
margin-top: 0;
background-color: $gray-light;
@@ -77,10 +81,6 @@
color: $gl-text-color;
}
- .oneline {
- line-height: 35px;
- }
-
> p:last-child {
margin-bottom: 0;
}
@@ -108,10 +108,6 @@
padding: 11px 0;
margin-bottom: 11px;
- .oneline {
- line-height: 35px;
- }
-
&.no-bottom-space {
border-bottom: 0;
margin-bottom: 0;
@@ -160,8 +156,6 @@
}
.cover-desc {
- color: $gl-text-color;
-
&.username:last-child {
padding-bottom: $gl-padding;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 2b499e50ea3..2e38b260f5f 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -439,10 +439,6 @@ img.emoji {
.min-height-0 { min-height: 0; }
-.w-3 { width: #{3 * $grid-size}; }
-
-.h-3 { width: #{3 * $grid-size}; }
-
.svg-w-100 {
svg {
width: 100%;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index a57cd6737f8..1bc597bd4ae 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -472,6 +472,22 @@
background-color: $blue-500;
}
}
+
+ .canary-badge {
+ .badge {
+ font-size: $gl-font-size-small;
+ line-height: $gl-line-height;
+ padding: 0 $grid-size;
+ }
+
+ &:hover {
+ text-decoration: none;
+
+ .badge {
+ text-decoration: none;
+ }
+ }
+ }
}
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 4a034e1e547..1cf122102cc 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -432,7 +432,7 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
*/
$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
'Courier New', 'andale mono', 'lucida console', monospace;
-$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
+$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans', Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
@@ -498,6 +498,17 @@ $pagination-line-height: 20px;
$pagination-disabled-color: #cdcdcd;
/*
+* Toasts
+*/
+$toast-offset: 24px;
+$toast-height: 48px;
+$toast-max-width: 586px;
+$toast-padding-right: 42px;
+$toast-default-margin: 8px;
+$toast-action-margin-left: 16px;
+$toast-background-opacity: 0.95;
+
+/*
* Status icons
*/
$status-icon-size: 22px;
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 47ffdbae4b6..f8e273a2735 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -14,7 +14,7 @@
}
.member {
- &.is-overriden {
+ &.is-overridden {
.btn-ldap-override {
display: none !important;
}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index d445be0eb19..d5bc723aa8c 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -89,6 +89,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
)
end
+ # Getting ToS url requires `directory` api call to Let's Encrypt
+ # which could result in 500 error/slow rendering on settings page
+ # Because of that we use separate controller action
+ def lets_encrypt_terms_of_service
+ redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url
+ end
+
private
def set_application_setting
diff --git a/app/controllers/concerns/project_unauthorized.rb b/app/controllers/concerns/project_unauthorized.rb
index d42363b8b17..7238840440f 100644
--- a/app/controllers/concerns/project_unauthorized.rb
+++ b/app/controllers/concerns/project_unauthorized.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
module ProjectUnauthorized
- def project_unauthorized_proc
- lambda do |project|
- if project
- label = project.external_authorization_classification_label
+ module ControllerActions
+ def self.on_routable_not_found
+ lambda do |routable|
+ return unless routable.is_a?(Project)
+
+ label = routable.external_authorization_classification_label
rejection_reason = nil
unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, label)
@@ -12,9 +14,7 @@ module ProjectUnauthorized
rejection_reason ||= _('External authorization denied access to this project')
end
- if rejection_reason
- access_denied!(rejection_reason)
- end
+ access_denied!(rejection_reason) if rejection_reason
end
end
end
diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb
index 5624eb3aa45..ff9b0332c97 100644
--- a/app/controllers/concerns/routable_actions.rb
+++ b/app/controllers/concerns/routable_actions.rb
@@ -3,15 +3,13 @@
module RoutableActions
extend ActiveSupport::Concern
- def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil, not_found_or_authorized_proc: nil)
+ def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path)
routable
else
- if not_found_or_authorized_proc
- not_found_or_authorized_proc.call(routable)
- end
+ perform_not_found_actions(routable, not_found_actions)
route_not_found unless performed?
@@ -19,6 +17,18 @@ module RoutableActions
end
end
+ def not_found_actions
+ [ProjectUnauthorized::ControllerActions.on_routable_not_found]
+ end
+
+ def perform_not_found_actions(routable, actions)
+ actions.each do |action|
+ break if performed?
+
+ instance_exec(routable, &action)
+ end
+ end
+
def routable_authorized?(routable, extra_authorization_proc)
return false unless routable
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 781eac7f080..80e4f54bbf4 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -3,7 +3,6 @@
class Projects::ApplicationController < ApplicationController
include CookiesHelper
include RoutableActions
- include ProjectUnauthorized
include ChecksCollaboration
skip_before_action :authenticate_user!
@@ -22,7 +21,7 @@ class Projects::ApplicationController < ApplicationController
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
- @project = find_routable!(Project, path, extra_authorization_proc: auth_proc, not_found_or_authorized_proc: project_unauthorized_proc)
+ @project = find_routable!(Project, path, extra_authorization_proc: auth_proc)
end
def build_canonical_path(project)
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
index c7b6218d007..2a04b007304 100644
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class Projects::Clusters::ApplicationsController < Clusters::ApplicationsController
- include ProjectUnauthorized
-
prepend_before_action :project
private
@@ -12,6 +10,6 @@ class Projects::Clusters::ApplicationsController < Clusters::ApplicationsControl
end
def project
- @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
+ @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]))
end
end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index feda6deeaa6..cb02581da37 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class Projects::ClustersController < Clusters::ClustersController
- include ProjectUnauthorized
-
prepend_before_action :project
before_action :repository
@@ -15,7 +13,7 @@ class Projects::ClustersController < Clusters::ClustersController
end
def project
- @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
+ @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]))
end
def repository
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 5a4adea497b..c342e1c80b0 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -221,7 +221,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def metrics_params
return unless Feature.enabled?(:metrics_time_window, project)
- return unless params[:start].present? || params[:end].present?
params.require([:start, :end])
end
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
index 8c3d141c888..79030da64d3 100644
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -3,8 +3,6 @@
module Projects
module Serverless
class FunctionsController < Projects::ApplicationController
- include ProjectUnauthorized
-
before_action :authorize_read_cluster!
def index
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 0fa4677ced1..07b38371ab9 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -4,6 +4,7 @@ class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
include AcceptsPendingInvitations
+ prepend_before_action :check_captcha, only: :create
before_action :whitelist_query_limiting, only: [:destroy]
before_action :ensure_terms_accepted,
if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms? },
@@ -21,15 +22,10 @@ class RegistrationsController < Devise::RegistrationsController
params[resource_name] = params.delete(:"new_#{resource_name}")
end
- if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
- accept_pending_invitations
- super do |new_user|
- persist_accepted_terms_if_required(new_user)
- end
- else
- flash[:alert] = s_('Profiles|There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
- flash.delete :recaptcha_error
- render action: 'new'
+ accept_pending_invitations
+
+ super do |new_user|
+ persist_accepted_terms_if_required(new_user)
end
rescue Gitlab::Access::AccessDeniedError
redirect_to(new_user_session_path)
@@ -89,6 +85,17 @@ class RegistrationsController < Devise::RegistrationsController
private
+ def check_captcha
+ return unless Feature.enabled?(:registrations_recaptcha, default_enabled: true)
+ return unless Gitlab::Recaptcha.load_configurations!
+
+ return if verify_recaptcha
+
+ flash[:alert] = _('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
+ flash.delete :recaptcha_error
+ render action: 'new'
+ end
+
def sign_up_params
params.require(:user).permit(:username, :email, :email_confirmation, :name, :password)
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 060b09f015c..5d28635232b 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -45,7 +45,7 @@ class UploadsController < ApplicationController
when Appearance
true
else
- permission = "read_#{model.class.to_s.underscore}".to_sym
+ permission = "read_#{model.class.underscore}".to_sym
can?(current_user, permission, model)
end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 1c3c24ad6dc..f7e49166ca0 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -46,7 +46,7 @@ module Resolvers
def resolve(**args)
# The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so
- # make sure it's loaded and not `nil` before continueing.
+ # make sure it's loaded and not `nil` before continuing.
project.sync if project.respond_to?(:sync)
return Issue.none if project.nil?
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index d90ef8903a7..42732eb93dd 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -21,6 +21,10 @@ module DashboardHelper
links.any? { |link| dashboard_nav_link?(link) }
end
+ def has_start_trial?
+ false
+ end
+
private
def get_dashboard_nav_links
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 96471d15aac..dc0e5511fcf 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -91,6 +91,28 @@ module EmailsHelper
].join(';')
end
+ def closure_reason_text(closed_via, format: nil)
+ case closed_via
+ when MergeRequest
+ merge_request = MergeRequest.find(closed_via[:id]).present
+
+ case format
+ when :html
+ " via merge request #{link_to(merge_request.to_reference, merge_request.web_url)}"
+ else
+ # If it's not HTML nor text then assume it's text to be safe
+ " via merge request #{merge_request.to_reference} (#{merge_request.web_url})"
+ end
+ when String
+ # Technically speaking this should be Commit but per
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339
+ # we can't deserialize Commit without custom serializer for ActiveJob
+ " via #{closed_via}"
+ else
+ ""
+ end
+ end
+
# "You are receiving this email because #{reason}"
def notification_reason_text(reason)
string = case reason
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 1371e9993b4..e990e425cb6 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -68,7 +68,7 @@ module EventsHelper
end
def event_preposition(event)
- if event.push? || event.commented? || event.target
+ if event.push_action? || event.commented_action? || event.target
"at"
elsif event.milestone?
"in"
@@ -80,11 +80,11 @@ module EventsHelper
words << event.author_name
words << event_action_name(event)
- if event.push?
+ if event.push_action?
words << event.ref_type
words << event.ref_name
words << "at"
- elsif event.commented?
+ elsif event.commented_action?
words << event.note_target_reference
words << "at"
elsif event.milestone?
@@ -121,9 +121,9 @@ module EventsHelper
if event.note_target
event_note_target_url(event)
end
- elsif event.push?
+ elsif event.push_action?
push_event_feed_url(event)
- elsif event.created_project?
+ elsif event.created_project_action?
project_url(event.project)
end
end
@@ -147,7 +147,7 @@ module EventsHelper
def event_feed_summary(event)
if event.issue?
render "events/event_issue", issue: event.issue
- elsif event.push?
+ elsif event.push_action?
render "events/event_push", event: event
elsif event.merge_request?
render "events/event_merge_request", merge_request: event.merge_request
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 05da5ebdb22..a57ba5f3a4f 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -58,6 +58,14 @@ module NavHelper
current_path?('milestones#show')
end
+ def admin_monitoring_nav_links
+ %w(system_info background_jobs logs health_check requests_profiles)
+ end
+
+ def group_issues_sub_menu_items
+ %w(groups#issues labels#index milestones#index boards#index boards#show)
+ end
+
private
def get_header_links
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 2c43b1a2067..91d15e0e4ea 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -315,6 +315,10 @@ module ProjectsHelper
) % { default_label: default_label }
end
+ def can_import_members?
+ Ability.allowed?(current_user, :admin_project_member, @project)
+ end
+
private
def get_project_nav_tabs(project, current_user)
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index 15041bd5805..e80b3f2b54a 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -2,6 +2,8 @@
module StorageHelper
def storage_counter(size_in_bytes)
+ return s_('StorageSize|Unknown') unless size_in_bytes
+
precision = size_in_bytes < 1.megabyte ? 0 : 1
number_to_human_size(size_in_bytes, delimiter: ',', precision: precision, significant: false)
diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb
index 7aa75ee30e6..cbaf53fced1 100644
--- a/app/mailers/devise_mailer.rb
+++ b/app/mailers/devise_mailer.rb
@@ -7,6 +7,7 @@ class DeviseMailer < Devise::Mailer
layout 'mailer/devise'
helper EmailsHelper
+ helper ApplicationHelper
protected
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index d2e334fb856..2b046d17122 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -30,8 +30,8 @@ module Emails
end
# rubocop: enable CodeReuse/ActiveRecord
- def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
- setup_issue_mail(issue_id, recipient_id)
+ def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason: nil, closed_via: nil)
+ setup_issue_mail(issue_id, recipient_id, closed_via: closed_via)
@updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
@@ -91,10 +91,11 @@ module Emails
private
- def setup_issue_mail(issue_id, recipient_id)
+ def setup_issue_mail(issue_id, recipient_id, closed_via: nil)
@issue = Issue.find(issue_id)
@project = @issue.project
@target_url = project_issue_url(@project, @issue)
+ @closed_via = closed_via
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 63148831a24..fc6ed695675 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -58,14 +58,14 @@ module Emails
}))
end
- def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
+ def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason: nil, closed_via: nil)
setup_merge_request_mail(merge_request_id, recipient_id)
@updated_by = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
- def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
+ def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason: nil, closed_via: nil)
setup_merge_request_mail(merge_request_id, recipient_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index 1e01f1d17e6..f355b02c428 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -53,7 +53,7 @@ class ActiveSession
def self.list(user)
Gitlab::Redis::SharedState.with do |redis|
- cleaned_up_lookup_entries(redis, user.id).map do |entry|
+ cleaned_up_lookup_entries(redis, user).map do |entry|
# rubocop:disable Security/MarshalLoad
Marshal.load(entry)
# rubocop:enable Security/MarshalLoad
@@ -78,7 +78,7 @@ class ActiveSession
def self.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
- cleaned_up_lookup_entries(redis, user.id)
+ cleaned_up_lookup_entries(redis, user)
end
end
@@ -90,25 +90,52 @@ class ActiveSession
"#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
end
- def self.cleaned_up_lookup_entries(redis, user_id)
- lookup_key = lookup_key_name(user_id)
+ def self.list_sessions(user)
+ sessions_from_ids(session_ids_for_user(user))
+ end
- session_ids = redis.smembers(lookup_key)
+ def self.session_ids_for_user(user)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.smembers(lookup_key_name(user.id))
+ end
+ end
- entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
- return [] if entry_keys.empty?
+ def self.sessions_from_ids(session_ids)
+ return [] if session_ids.empty?
- entries = redis.mget(entry_keys)
+ Gitlab::Redis::SharedState.with do |redis|
+ session_keys = session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
- session_ids_and_entries = session_ids.zip(entries)
+ redis.mget(session_keys).compact.map do |raw_session|
+ # rubocop:disable Security/MarshalLoad
+ Marshal.load(raw_session)
+ # rubocop:enable Security/MarshalLoad
+ end
+ end
+ end
+
+ def self.raw_active_session_entries(session_ids, user_id)
+ return [] if session_ids.empty?
+
+ Gitlab::Redis::SharedState.with do |redis|
+ entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
+
+ redis.mget(entry_keys)
+ end
+ end
+
+ def self.cleaned_up_lookup_entries(redis, user)
+ session_ids = session_ids_for_user(user)
+ entries = raw_active_session_entries(session_ids, user.id)
# remove expired keys.
# only the single key entries are automatically expired by redis, the
# lookup entries in the set need to be removed manually.
+ session_ids_and_entries = session_ids.zip(entries)
session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry|
- redis.srem(lookup_key, session_id)
+ redis.srem(lookup_key_name(user.id), session_id)
end
- session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry }
+ entries.compact
end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index d1d01368972..0979d03f6e6 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -41,4 +41,8 @@ class ApplicationRecord < ActiveRecord::Base
find_or_create_by(*args)
end
end
+
+ def self.underscore
+ Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { self.to_s.underscore }
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d2f5ff13408..1b67a7272bc 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -140,6 +140,8 @@ module Ci
where("EXISTS (?)", matcher)
end
+ scope :queued_before, ->(time) { where(arel_table[:queued_at].lt(time)) }
+
##
# TODO: Remove these mounters when we remove :ci_enable_legacy_artifacts feature flag
mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file
@@ -378,8 +380,6 @@ module Ci
end
def any_unmet_prerequisites?
- return false unless Feature.enabled?(:ci_preparing_state, default_enabled: true)
-
prerequisites.present?
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index f9cf398556d..3beb76ffc2b 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -99,7 +99,7 @@ module Ci
raw: 1,
zip: 2,
gzip: 3
- }
+ }, _suffix: true
# `file_location` indicates where actual files are stored.
# Ideally, actual files should be stored in the same directory, and use the same
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index 987c057ad6d..36c51522089 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -39,7 +39,7 @@ module Clusters
end
# Will be addressed in future MRs
- # We need to investigate and document what will be permenantly deleted.
+ # We need to investigate and document what will be permanently deleted.
def allowed_to_uninstall?
false
end
diff --git a/app/models/clusters/concerns/application_data.rb b/app/models/clusters/concerns/application_data.rb
index a48ee340fac..3479fea415e 100644
--- a/app/models/clusters/concerns/application_data.rb
+++ b/app/models/clusters/concerns/application_data.rb
@@ -3,56 +3,52 @@
module Clusters
module Concerns
module ApplicationData
- extend ActiveSupport::Concern
-
- included do
- def uninstall_command
- Gitlab::Kubernetes::Helm::DeleteCommand.new(
- name: name,
- rbac: cluster.platform_kubernetes_rbac?,
- files: files
- )
- end
+ def uninstall_command
+ Gitlab::Kubernetes::Helm::DeleteCommand.new(
+ name: name,
+ rbac: cluster.platform_kubernetes_rbac?,
+ files: files
+ )
+ end
- def repository
- nil
- end
+ def repository
+ nil
+ end
- def values
- File.read(chart_values_file)
- end
+ def values
+ File.read(chart_values_file)
+ end
- def files
- @files ||= begin
- files = { 'values.yaml': values }
+ def files
+ @files ||= begin
+ files = { 'values.yaml': values }
- files.merge!(certificate_files) if cluster.application_helm.has_ssl?
+ files.merge!(certificate_files) if cluster.application_helm.has_ssl?
- files
- end
+ files
end
+ end
- private
+ private
- def certificate_files
- {
- 'ca.pem': ca_cert,
- 'cert.pem': helm_cert.cert_string,
- 'key.pem': helm_cert.key_string
- }
- end
+ def certificate_files
+ {
+ 'ca.pem': ca_cert,
+ 'cert.pem': helm_cert.cert_string,
+ 'key.pem': helm_cert.key_string
+ }
+ end
- def ca_cert
- cluster.application_helm.ca_cert
- end
+ def ca_cert
+ cluster.application_helm.ca_cert
+ end
- def helm_cert
- @helm_cert ||= cluster.application_helm.issue_client_cert
- end
+ def helm_cert
+ @helm_cert ||= cluster.application_helm.issue_client_cert
+ end
- def chart_values_file
- "#{Rails.root}/vendor/#{name}/values.yaml"
- end
+ def chart_values_file
+ "#{Rails.root}/vendor/#{name}/values.yaml"
end
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 593acf5edfe..738080eb584 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -68,7 +68,7 @@ class Event < ApplicationRecord
# Callbacks
after_create :reset_project_activity
- after_create :set_last_repository_updated_at, if: :push?
+ after_create :set_last_repository_updated_at, if: :push_action?
after_create :track_user_interacted_projects
# Scopes
@@ -138,11 +138,11 @@ class Event < ApplicationRecord
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def visible_to_user?(user = nil)
- if push? || commit_note?
+ if push_action? || commit_note?
Ability.allowed?(user, :download_code, project)
elsif membership_changed?
Ability.allowed?(user, :read_project, project)
- elsif created_project?
+ elsif created_project_action?
Ability.allowed?(user, :read_project, project)
elsif issue? || issue_note?
Ability.allowed?(user, :read_issue, note? ? note_target : target)
@@ -173,56 +173,56 @@ class Event < ApplicationRecord
target.try(:title)
end
- def created?
+ def created_action?
action == CREATED
end
- def push?
+ def push_action?
false
end
- def merged?
+ def merged_action?
action == MERGED
end
- def closed?
+ def closed_action?
action == CLOSED
end
- def reopened?
+ def reopened_action?
action == REOPENED
end
- def joined?
+ def joined_action?
action == JOINED
end
- def left?
+ def left_action?
action == LEFT
end
- def expired?
+ def expired_action?
action == EXPIRED
end
- def destroyed?
+ def destroyed_action?
action == DESTROYED
end
- def commented?
+ def commented_action?
action == COMMENTED
end
def membership_changed?
- joined? || left? || expired?
+ joined_action? || left_action? || expired_action?
end
- def created_project?
- created? && !target && target_type.nil?
+ def created_project_action?
+ created_action? && !target && target_type.nil?
end
def created_target?
- created? && target
+ created_action? && target
end
def milestone?
@@ -258,23 +258,23 @@ class Event < ApplicationRecord
end
def action_name
- if push?
+ if push_action?
push_action_name
- elsif closed?
+ elsif closed_action?
"closed"
- elsif merged?
+ elsif merged_action?
"accepted"
- elsif joined?
+ elsif joined_action?
'joined'
- elsif left?
+ elsif left_action?
'left'
- elsif expired?
+ elsif expired_action?
'removed due to membership expiration from'
- elsif destroyed?
+ elsif destroyed_action?
'destroyed'
- elsif commented?
+ elsif commented_action?
"commented on"
- elsif created_project?
+ elsif created_project_action?
created_project_action_name
else
"opened"
@@ -337,7 +337,7 @@ class Event < ApplicationRecord
end
def body?
- if push?
+ if push_action?
push_with_commits?
elsif note?
true
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index b266c61f002..4cba69069bb 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -12,7 +12,7 @@ class GroupMember < Member
validates :source_type, format: { with: /\ANamespace\z/ }
default_scope { where(source_type: SOURCE_TYPE) }
- scope :in_groups, ->(groups) { where(source_id: groups.select(:id)) }
+ scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
scope :count_users_by_group_id, -> { joins(:user).group(:source_id).count }
diff --git a/app/models/project.rb b/app/models/project.rb
index 61d245478ca..ab4da61dcf8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -56,7 +56,6 @@ class Project < ApplicationRecord
VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
ignore_column :import_status, :import_jid, :import_error
- ignore_column :ci_id
cache_markdown_field :description, pipeline: :description
@@ -337,8 +336,8 @@ class Project < ApplicationRecord
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_personal_projects_limit, on: :create
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
- validate :visibility_level_allowed_by_group, if: -> { changes.has_key?(:visibility_level) }
- validate :visibility_level_allowed_as_fork, if: -> { changes.has_key?(:visibility_level) }
+ validate :visibility_level_allowed_by_group, if: :should_validate_visibility_level?
+ validate :visibility_level_allowed_as_fork, if: :should_validate_visibility_level?
validate :check_wiki_path_conflict
validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
validates :repository_storage,
@@ -892,6 +891,10 @@ class Project < ApplicationRecord
self.errors.add(:limit_reached, error % { limit: limit })
end
+ def should_validate_visibility_level?
+ new_record? || changes.has_key?(:visibility_level)
+ end
+
def visibility_level_allowed_by_group
return if visibility_level_allowed_by_group?
diff --git a/app/models/push_event.rb b/app/models/push_event.rb
index 9c0267c3140..4698df39730 100644
--- a/app/models/push_event.rb
+++ b/app/models/push_event.rb
@@ -69,7 +69,7 @@ class PushEvent < Event
PUSHED
end
- def push?
+ def push_action?
true
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 1c02e68f2f6..e05d3dd58ac 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -283,14 +283,19 @@ class Repository
end
def diverging_commit_counts(branch)
+ return diverging_commit_counts_without_max(branch) if Feature.enabled?('gitaly_count_diverging_commits_no_max')
+
+ ## TODO: deprecate the below code after 12.0
@root_ref_hash ||= raw_repository.commit(root_ref).id
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
# Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes
+ branch_sha = branch.dereferenced_target.sha
+
number_commits_behind, number_commits_ahead =
raw_repository.diverging_commit_count(
@root_ref_hash,
- branch.dereferenced_target.sha,
+ branch_sha,
max_count: MAX_DIVERGING_COUNT)
if number_commits_behind + number_commits_ahead >= MAX_DIVERGING_COUNT
@@ -301,6 +306,22 @@ class Repository
end
end
+ def diverging_commit_counts_without_max(branch)
+ @root_ref_hash ||= raw_repository.commit(root_ref).id
+ cache.fetch(:"diverging_commit_counts_without_max_#{branch.name}") do
+ # Rugged seems to throw a `ReferenceError` when given branch_names rather
+ # than SHA-1 hashes
+ branch_sha = branch.dereferenced_target.sha
+
+ number_commits_behind, number_commits_ahead =
+ raw_repository.diverging_commit_count(
+ @root_ref_hash,
+ branch_sha)
+
+ { behind: number_commits_behind, ahead: number_commits_ahead }
+ end
+ end
+
def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:, path: nil)
raw_repository.archive_metadata(
ref,
@@ -1050,7 +1071,7 @@ class Repository
# To support the full deprecated behaviour, set the
# `rebase_commit_sha` for the merge_request here and return the value
- merge_request.update(rebase_commit_sha: rebase_sha)
+ merge_request.update(rebase_commit_sha: rebase_sha, merge_error: nil)
rebase_sha
end
@@ -1069,7 +1090,7 @@ class Repository
remote_repository: merge_request.target_project.repository.raw,
remote_branch: merge_request.target_branch
) do |commit_id|
- merge_request.update!(rebase_commit_sha: commit_id)
+ merge_request.update!(rebase_commit_sha: commit_id, merge_error: nil)
end
end
end
diff --git a/app/serializers/test_case_entity.rb b/app/serializers/test_case_entity.rb
index ec60055ba5b..5c915c1302c 100644
--- a/app/serializers/test_case_entity.rb
+++ b/app/serializers/test_case_entity.rb
@@ -3,6 +3,7 @@
class TestCaseEntity < Grape::Entity
expose :status
expose :name
+ expose :classname
expose :execution_time
expose :system_output
expose :stack_trace
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 6707a1363d0..baa3f898b2d 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -36,6 +36,11 @@ module Ci
builds = builds.with_any_tags
end
+ # pick builds that older than specified age
+ if params.key?(:job_age)
+ builds = builds.queued_before(params[:job_age].seconds.ago)
+ end
+
builds.each do |build|
next unless runner.can_pick?(build)
diff --git a/app/services/clusters/refresh_service.rb b/app/services/clusters/refresh_service.rb
index b02bb9c0247..3752a306793 100644
--- a/app/services/clusters/refresh_service.rb
+++ b/app/services/clusters/refresh_service.rb
@@ -21,11 +21,7 @@ module Clusters
private_class_method :projects_with_missing_kubernetes_namespaces_for_cluster
def self.clusters_with_missing_kubernetes_namespaces_for_project(project)
- if Feature.enabled?(:ci_preparing_state, default_enabled: true)
- project.clusters.managed.missing_kubernetes_namespace(project.kubernetes_namespaces)
- else
- project.all_clusters.managed.missing_kubernetes_namespace(project.kubernetes_namespaces)
- end
+ project.clusters.managed.missing_kubernetes_namespace(project.kubernetes_namespaces)
end
private_class_method :clusters_with_missing_kubernetes_namespaces_for_project
diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb
index a3cc6014fd3..1c828234f1b 100644
--- a/app/services/concerns/users/participable_service.rb
+++ b/app/services/concerns/users/participable_service.rb
@@ -29,7 +29,7 @@ module Users
def groups
group_counts = GroupMember
- .in_groups(current_user.authorized_groups)
+ .of_groups(current_user.authorized_groups)
.non_request
.count_users_by_group_id
diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb
index 9d371e234ee..d30df34e54b 100644
--- a/app/services/git/base_hooks_service.rb
+++ b/app/services/git/base_hooks_service.rb
@@ -17,6 +17,8 @@ module Git
# Not a hook, but it needs access to the list of changed commits
enqueue_invalidate_cache
+ update_remote_mirrors
+
push_data
end
@@ -92,5 +94,12 @@ module Git
def pipeline_options
{}
end
+
+ def update_remote_mirrors
+ return unless project.has_remote_mirror?
+
+ project.mark_stuck_remote_mirrors_as_failed!
+ project.update_remote_mirrors
+ end
end
end
diff --git a/app/services/git/branch_push_service.rb b/app/services/git/branch_push_service.rb
index abf11f253f6..c4910180787 100644
--- a/app/services/git/branch_push_service.rb
+++ b/app/services/git/branch_push_service.rb
@@ -27,7 +27,6 @@ module Git
execute_related_hooks
perform_housekeeping
- update_remote_mirrors
stop_environments
true
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index e5cc12e6082..2a19e57a94f 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -7,7 +7,7 @@ module Issues
return issue unless can?(current_user, :update_issue, issue)
close_issue(issue,
- commit: commit,
+ closed_via: commit,
notifications: notifications,
system_note: system_note)
end
@@ -17,9 +17,9 @@ module Issues
#
# The code calling this method is responsible for ensuring that a user is
# allowed to close the given issue.
- def close_issue(issue, commit: nil, notifications: true, system_note: true)
+ def close_issue(issue, closed_via: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active && issue.is_a?(ExternalIssue)
- project.jira_service.close_issue(commit, issue)
+ project.jira_service.close_issue(closed_via, issue)
todo_service.close_issue(issue, current_user)
return issue
end
@@ -27,8 +27,11 @@ module Issues
if project.issues_enabled? && issue.close
issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user)
- create_note(issue, commit) if system_note
- notification_service.async.close_issue(issue, current_user) if notifications
+ create_note(issue, closed_via) if system_note
+
+ closed_via = "commit #{closed_via.id}" if closed_via.is_a?(Commit)
+
+ notification_service.async.close_issue(issue, current_user, closed_via: closed_via) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index f9717a9426b..c8d5e563cd8 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -45,7 +45,7 @@ module Members
def delete_subgroup_members(member)
groups = member.group.descendants
- GroupMember.in_groups(groups).with_user(member.user).each do |group_member|
+ GroupMember.of_groups(groups).with_user(member.user).each do |group_member|
self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true)
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 8d3b569498f..f797c0f11c6 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -89,8 +89,8 @@ class NotificationService
# * project team members with notification level higher then Participating
# * users with custom level checked with "close issue"
#
- def close_issue(issue, current_user)
- close_resource_email(issue, current_user, :closed_issue_email)
+ def close_issue(issue, current_user, closed_via: nil)
+ close_resource_email(issue, current_user, :closed_issue_email, closed_via: closed_via)
end
# When we reassign an issue we should send an email to:
@@ -504,7 +504,7 @@ class NotificationService
end
end
- def close_resource_email(target, current_user, method, skip_current_user: true)
+ def close_resource_email(target, current_user, method, skip_current_user: true, closed_via: nil)
action = method == :merged_merge_request_email ? "merge" : "close"
recipients = NotificationRecipientService.build_recipients(
@@ -515,7 +515,7 @@ class NotificationService
)
recipients.each do |recipient|
- mailer.send(method, recipient.user.id, target.id, current_user.id, recipient.reason).deliver_later
+ mailer.send(method, recipient.user.id, target.id, current_user.id, reason: recipient.reason, closed_via: closed_via).deliver_later
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 4ea40e3c8ce..9f335cceb67 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -100,8 +100,6 @@ module Projects
current_user.invalidate_personal_projects_count
create_readme if @initialize_with_readme
-
- configure_group_clusters_for_project
end
# Refresh the current user's authorizations inline (so they can access the
@@ -127,10 +125,6 @@ module Projects
Files::CreateService.new(@project, current_user, commit_attrs).execute
end
- def configure_group_clusters_for_project
- ClusterProjectConfigureWorker.perform_async(@project.id)
- end
-
def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 91c01eca75c..233dcf37e35 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -54,7 +54,6 @@ module Projects
end
attempt_transfer_transaction
- configure_group_clusters_for_project
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -164,9 +163,5 @@ module Projects
@new_namespace.full_path
)
end
-
- def configure_group_clusters_for_project
- ClusterProjectConfigureWorker.perform_async(project.id)
- end
end
end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index 0a166335b4e..b488bba00e9 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -9,6 +9,6 @@ class AttachmentUploader < GitlabUploader
private
def dynamic_segment
- File.join(model.class.to_s.underscore, mounted_as.to_s, model.id.to_s)
+ File.join(model.class.underscore, mounted_as.to_s, model.id.to_s)
end
end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index c0165759203..9af59b0aceb 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -25,6 +25,6 @@ class AvatarUploader < GitlabUploader
private
def dynamic_segment
- File.join(model.class.to_s.underscore, mounted_as.to_s, model.id.to_s)
+ File.join(model.class.underscore, mounted_as.to_s, model.id.to_s)
end
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 6dfe2bed0ba..1c7582533ad 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -109,12 +109,20 @@ class FileUploader < GitlabUploader
def upload_path
if file_storage?
# Legacy path relative to project.full_path
- File.join(dynamic_segment, identifier)
+ local_storage_path(identifier)
else
- File.join(store_dir, identifier)
+ remote_storage_path(identifier)
end
end
+ def local_storage_path(file_identifier)
+ File.join(dynamic_segment, file_identifier)
+ end
+
+ def remote_storage_path(file_identifier)
+ File.join(store_dir, file_identifier)
+ end
+
def store_dirs
{
Store::LOCAL => File.join(base_dir, dynamic_segment),
diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb
index 272837aa6ce..b43162f0935 100644
--- a/app/uploaders/personal_file_uploader.rb
+++ b/app/uploaders/personal_file_uploader.rb
@@ -6,21 +6,18 @@ class PersonalFileUploader < FileUploader
options.storage_path
end
- def self.base_dir(model, store = nil)
- base_dirs(model)[store || Store::LOCAL]
- end
-
- def self.base_dirs(model)
- {
- Store::LOCAL => File.join(options.base_dir, model_path_segment(model)),
- Store::REMOTE => model_path_segment(model)
- }
+ def self.base_dir(model, _store = nil)
+ # base_dir is the path seen by the user when rendering Markdown, so
+ # it should be the same for both local and object storage. It is
+ # typically prefaced with uploads/-/system, but that prefix
+ # is omitted in the path stored on disk.
+ File.join(options.base_dir, model_path_segment(model))
end
def self.model_path_segment(model)
return 'temp/' unless model
- File.join(model.class.to_s.underscore, model.id.to_s)
+ File.join(model.class.underscore, model.id.to_s)
end
def object_store
@@ -40,8 +37,61 @@ class PersonalFileUploader < FileUploader
store_dirs[object_store]
end
+ # A personal snippet path is stored using FileUploader#upload_path.
+ #
+ # The format for the path:
+ #
+ # Local storage: :random_hex/:filename.
+ # Object storage: personal_snippet/:id/:random_hex/:filename.
+ #
+ # upload_paths represent the possible paths for a given identifier,
+ # which will vary depending on whether the file is stored in local or
+ # object storage. upload_path should match an element in upload_paths.
+ #
+ # base_dir represents the path seen by the user in Markdown, and it
+ # should always be prefixed with uploads/-/system.
+ #
+ # store_dirs represent the paths that are actually used on disk. For
+ # object storage, this should omit the prefix /uploads/-/system.
+ #
+ # For example, consider the requested path /uploads/-/system/personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20/file.png.
+ #
+ # For local storage:
+ #
+ # File on disk: /opt/gitlab/embedded/service/gitlab-rails/public/uploads/-/system/personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20/file.png.
+ #
+ # base_dir: uploads/-/system/personal_snippet/172
+ # upload_path: ff4ad5c2e40b39ae57cda51577317d20/file.png
+ # upload_paths: ["ff4ad5c2e40b39ae57cda51577317d20/file.png", "personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20/file.png"].
+ # store_dirs:
+ # => {1=>"uploads/-/system/personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20", 2=>"personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20"}
+ #
+ # For object storage:
+ #
+ # upload_path: personal_snippet/172/ff4ad5c2e40b39ae57cda51577317d20/file.png
+ def upload_paths(identifier)
+ [
+ local_storage_path(identifier),
+ File.join(remote_storage_base_path, identifier)
+ ]
+ end
+
+ def store_dirs
+ {
+ Store::LOCAL => File.join(base_dir, dynamic_segment),
+ Store::REMOTE => remote_storage_base_path
+ }
+ end
+
private
+ # To avoid prefacing the remote storage path with `/uploads/-/system`,
+ # we just drop that part so that the destination path will be
+ # personal_snippet/:id/:random_hex/:filename.
+ def remote_storage_base_path
+ File.join(self.class.model_path_segment(model), dynamic_segment)
+ end
+
def secure_url
File.join('/', base_dir, secret, file.filename)
end
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index 64e01fa2d00..77795dbf913 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -30,8 +30,7 @@
.form-check
= f.check_box :lets_encrypt_terms_of_service_accepted, class: 'form-check-input'
= f.label :lets_encrypt_terms_of_service_accepted, class: 'form-check-label' do
- // Terms of Service should actually be a link, but the best way to get the url is using API
- // So it will be done in later MR
- = _("I have read and agree to the Let's Encrypt Terms of Service")
+ - terms_of_service_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: lets_encrypt_terms_of_service_admin_application_settings_path }
+ = _("I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end}").html_safe % { link_start: terms_of_service_link_start, link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 8fb38f6a690..62d1d01cc83 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -2,7 +2,7 @@
= form_errors(@group)
= render 'shared/group_form', f: f
- = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
+ = render_if_exists 'shared/old_repository_size_limit_setting', form: f, type: :group
= render_if_exists 'admin/namespace_plan', f: f
.form-group.row.group-description-holder
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index 0f5e97e288a..ac56e354a4d 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -23,7 +23,7 @@
%code= liveness_url(token: Gitlab::CurrentSettings.health_check_access_token)
%li
%code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token)
-
+ = render_if_exists 'admin/health_check/health_check_url'
%hr
.card
.card-header
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index 5bc695aa7b5..9117f63f939 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -13,7 +13,7 @@
.stats
%span.badge.badge-pill
- = storage_counter(project.statistics.storage_size)
+ = storage_counter(project.statistics&.storage_size)
- if project.archived
%span.badge.badge-warning archived
.title
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index f016a157daf..1e1ad9d5e19 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -74,10 +74,10 @@
%li
%span.light= _('Storage:')
- %strong= storage_counter(@project.statistics.storage_size)
- (
- = storage_counters_details(@project.statistics)
- )
+ %strong= storage_counter(@project.statistics&.storage_size)
+ - if @project.statistics
+ = surround '(', ')' do
+ = storage_counters_details(@project.statistics)
%li
%span.light last commit:
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 12e24ddef02..98b6bc7bc46 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -22,6 +22,8 @@
%p.light
Regular users have access to their groups and projects
+ = render_if_exists 'admin/users/auditor_access_level_radio', f: f, disabled: editing_current_user
+
= f.radio_button :access_level, :admin, disabled: editing_current_user
= label_tag :admin, class: 'font-weight-bold' do
Admin
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index a733f420d11..e7dde7985fd 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -6,6 +6,7 @@
%span.cred (Internal)
- if @user.admin
%span.cred (Admin)
+ = render_if_exists 'admin/users/audtior_user_badge'
.float-right
- if impersonation_enabled? && @user != current_user && @user.can?(:log_in)
diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml
index 8caa25a7b5e..c1727cf9079 100644
--- a/app/views/clusters/platforms/kubernetes/_form.html.haml
+++ b/app/views/clusters/platforms/kubernetes/_form.html.haml
@@ -1,7 +1,7 @@
= bootstrap_form_for cluster, url: update_cluster_url_path, html: { class: 'gl-show-field-errors' },
as: :cluster do |field|
- copy_name_btn = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
- class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
+ class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= field.text_field :name, class: 'js-select-on-focus cluster-name', required: true,
title: s_('ClusterIntegration|Cluster name is required.'),
readonly: cluster.read_only_kubernetes_platform_fields?,
@@ -10,7 +10,7 @@
= field.fields_for :platform_kubernetes, platform do |platform_field|
- copy_api_url = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
- class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
+ class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.text_field :api_url, class: 'js-select-on-focus', required: true,
title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
readonly: cluster.read_only_kubernetes_platform_fields?,
@@ -18,7 +18,7 @@
input_group_class: 'gl-field-error-anchor', append: copy_api_url
- copy_ca_cert_btn = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
- class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
+ class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.text_area :ca_cert, class: 'js-select-on-focus', rows: '5',
readonly: cluster.read_only_kubernetes_platform_fields?,
placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
@@ -28,7 +28,7 @@
- show_token_btn = (platform_field.button s_('ClusterIntegration|Show'),
type: 'button', class: 'js-show-cluster-token btn btn-default')
- copy_token_btn = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Service Token'),
- class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
+ class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.text_field :token, type: 'password', class: 'js-select-on-focus js-cluster-token',
required: true, title: s_('ClusterIntegration|Service token is required.'),
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index 18a82feb189..8933c5d7227 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,4 @@
-.blank-state-parent-container
+.blank-state-parent-container{ class: ('has-start-trial-container' if has_start_trial?) }
.section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
.container.section-body
.row
@@ -7,7 +7,12 @@
Welcome to GitLab
%p.blank-state-text
Code, test, and deploy together
- - if current_user.admin?
- = render "blank_state_admin_welcome"
- - else
- = render "blank_state_welcome"
+ .blank-state-row
+ %div{ class: ('column-large' if has_start_trial?) }
+ - if current_user.admin?
+ = render "blank_state_admin_welcome"
+ - else
+ = render "blank_state_welcome"
+ - if has_start_trial?
+ .column-small
+ = render_if_exists "blank_state_ee_trial"
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 9c7ca6ebbd4..427db070253 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -29,6 +29,7 @@
- terms_link = link_to s_("I accept the|Terms of Service and Privacy Policy"), terms_path, target: "_blank"
- accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link }
= accept_terms_label.html_safe
+ = render_if_exists 'devise/shared/email_opted_in', f: f
%div
- if Gitlab::Recaptcha.enabled?
= recaptcha_tags
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index aee05b6c81c..b1a9470cf1c 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -2,6 +2,7 @@
- if crowd_enabled?
%li.nav-item
= link_to "Crowd", "#crowd", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:crowd))}", 'data-toggle' => 'tab'
+ = render_if_exists "devise/shared/kerberos_tab"
- @ldap_servers.each_with_index do |server, i|
%li.nav-item
= link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain))} qa-ldap-tab", 'data-toggle' => 'tab'
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 2fcb1d1fd2b..222175c818a 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -3,11 +3,11 @@
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
- - if event.created_project?
+ - if event.created_project_action?
= render "events/event/created_project", event: event
- - elsif event.push?
+ - elsif event.push_action?
= render "events/event/push", event: event
- - elsif event.commented?
+ - elsif event.commented_action?
= render "events/event/note", event: event
- else
= render "events/event/common", event: event
diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml
index c382a1ed168..e12748666c8 100644
--- a/app/views/groups/settings/_general.html.haml
+++ b/app/views/groups/settings/_general.html.haml
@@ -17,17 +17,17 @@
= f.label :description, _('Group description (optional)'), class: 'label-bold'
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
- = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
+ = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
- .form-group.prepend-top-default.append-bottom-20
- .avatar-container.rect-avatar.s90
- = group_icon(@group, alt: '', class: 'avatar group-avatar s90')
- = f.label :avatar, _('Group avatar'), class: 'label-bold d-block'
- = render 'shared/choose_avatar_button', f: f
- - if @group.avatar?
- %hr
- = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link'
+ .form-group.prepend-top-default.append-bottom-20
+ .avatar-container.rect-avatar.s90
+ = group_icon(@group, alt: '', class: 'avatar group-avatar s90')
+ = f.label :avatar, _('Group avatar'), class: 'label-bold d-block'
+ = render 'shared/choose_avatar_button', f: f
+ - if @group.avatar?
+ %hr
+ = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-link'
- = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+ = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
= f.submit _('Save changes'), class: 'btn btn-success mt-4 js-dirty-submit'
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 75e4dc46c9b..50933c7d434 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -5,8 +5,7 @@
%hr
%h1
- GitLab
- Community Edition
+ = default_brand_title
- if user_signed_in?
%span= link_to_version
= version_status_badge
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 969df69aafb..cdc894ee5a0 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -70,7 +70,7 @@
.cover-title
John Smith
- .cover-desc
+ .cover-desc.cgray
= lorem
.cover-controls
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 11e83ddfe64..c357207054b 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -77,3 +77,4 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
+ = render_if_exists 'layouts/snowplow'
diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml
index e13490ed410..6e8294d6adc 100644
--- a/app/views/layouts/_mailer.html.haml
+++ b/app/views/layouts/_mailer.html.haml
@@ -64,6 +64,8 @@
%tbody
= yield
+ = render_if_exists 'layouts/mailer/additional_text'
+
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 26a1f1e119c..006334ade07 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -5,6 +5,7 @@
= render 'shared/outdated_browser'
.mobile-overlay
.alert-wrapper
+ = render_if_exists "layouts/header/ee_license_banner"
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
= render "layouts/nav/classification_level_banner"
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 2f3c13aaf6e..f7a561afbb3 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -26,6 +26,9 @@
- if Gitlab::CurrentSettings.sign_in_text.present?
= markdown_field(Gitlab::CurrentSettings.current_application_settings, :sign_in_text)
+
+ = render_if_exists 'layouts/devise_help_text'
+
.col-sm-5.new-session-forms-container
= yield
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 724c9976954..f8b7d0c530a 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -18,8 +18,9 @@
%span.logo-text.d-none.d-lg-block.prepend-left-8
= logo_text
- if Gitlab.com?
- %span.js-canary-badge.badge.badge-pill.green-badge.align-self-center
- = _('Next')
+ = link_to 'https://next.gitlab.com', class: 'label-link js-canary-badge canary-badge bg-transparent hidden', target: :_blank do
+ %span.color-label.has-tooltip.badge.badge-pill.green-badge
+ = _('Next')
- if current_user
= render "layouts/nav/dashboard"
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index fbec62b02f8..5643a508ddc 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -8,6 +8,7 @@
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
= render 'shared/user_dropdown_contributing_link'
+ = render_if_exists 'shared/user_dropdown_instance_review'
- if Gitlab.com?
%li.js-canary-link
= link_to _("Switch to GitLab Next"), "https://next.gitlab.com/"
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
index f8032f3262b..1a06ea68bcd 100644
--- a/app/views/layouts/mailer.text.erb
+++ b/app/views/layouts/mailer.text.erb
@@ -4,5 +4,6 @@
-- <%# signature marker %>
<%= _("You're receiving this email because of your account on %{host}.") % { host: Gitlab.config.gitlab.host } %>
+<%= render_if_exists 'layouts/mailer/additional_text' %>
<%= text_footer_message %>
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 5a27237bf76..47710b9e9e5 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -95,3 +95,4 @@
= link_to sherlock_transactions_path, class: 'admin-icon d-none d-lg-block d-xl-block', title: _('Sherlock Transactions'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
+ = render_if_exists 'layouts/nav/geo_primary_node_url'
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 04d67e024ba..83fe871285a 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -48,7 +48,7 @@
%span
= _('Gitaly Servers')
- = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do
+ = nav_link(controller: admin_monitoring_nav_links) do
= link_to admin_system_info_path do
.nav-icon-container
= sprite_icon('monitor')
@@ -81,6 +81,7 @@
= link_to admin_requests_profiles_path, title: _('Requests Profiles') do
%span
= _('Requests Profiles')
+ = render_if_exists 'layouts/nav/ee/admin/new_monitoring_sidebar'
= nav_link(controller: :broadcast_messages) do
= link_to admin_broadcast_messages_path do
@@ -132,6 +133,8 @@
= _('Abuse Reports')
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all))
+ = render_if_exists 'layouts/nav/sidebar/licenses_link'
+
- if instance_clusters_enabled?
= nav_link(controller: :clusters) do
= link_to admin_clusters_path do
@@ -158,6 +161,10 @@
%strong.fly-out-top-item-name
= _('Spam Logs')
+ = render_if_exists 'layouts/nav/sidebar/push_rules_link'
+
+ = render_if_exists 'layouts/nav/ee/admin/geo_sidebar'
+
= nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path do
.nav-icon-container
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index c2116ec63dd..0fc5ebbea7e 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -1,6 +1,5 @@
- issues_count = group_issues_count(state: 'opened')
- merge_requests_count = group_merge_requests_count(state: 'opened')
-- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index', 'boards#index', 'boards#show']
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
@@ -51,7 +50,7 @@
= render_if_exists "layouts/nav/ee/epic_link", group: @group
- if group_sidebar_link?(:issues)
- = nav_link(path: issues_sub_menu_items) do
+ = nav_link(path: group_issues_sub_menu_items) do
= link_to issues_group_path(@group) do
.nav-icon-container
= sprite_icon('issues')
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 2061eac917f..7dd33f3c641 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -28,6 +28,8 @@
= link_to profile_account_path do
%strong.fly-out-top-item-name
= _('Account')
+
+ = render_if_exists 'layouts/nav/sidebar/profile_billing_link'
= nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path do
.nav-icon-container
@@ -151,4 +153,6 @@
%strong.fly-out-top-item-name
= _('Authentication Log')
+ = render_if_exists 'layouts/nav/sidebar/profile_pipeline_quota_link'
+
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 3a0c2b9c284..399305baec1 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -270,6 +270,8 @@
%span= _("Got it!")
= sprite_icon('thumb-up')
+ = render_if_exists 'layouts/nav/sidebar/project_feature_flags_link'
+
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
@@ -283,7 +285,9 @@
%strong.fly-out-top-item-name
= _('Registry')
- - if project_nav_tab?(:wiki)
+ = render_if_exists 'layouts/nav/sidebar/project_packages_link'
+
+ - if project_nav_tab? :wiki
- wiki_url = project_wiki_path(@project, :home)
= nav_link(controller: :wikis) do
= link_to wiki_url, class: 'shortcuts-wiki qa-wiki-link' do
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 8dff12c1b7f..de487a94d40 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -31,4 +31,6 @@
adjust your notification settings.
= email_action @target_url
+
+ = render_if_exists 'layouts/email_additional_text'
= html_footer_message
diff --git a/app/views/layouts/notify.text.erb b/app/views/layouts/notify.text.erb
index 248916fba63..0ee30c2a6cf 100644
--- a/app/views/layouts/notify.text.erb
+++ b/app/views/layouts/notify.text.erb
@@ -12,5 +12,6 @@
<% end -%>
<%= "You're receiving this email because #{notification_reason_text(@reason)}." %>
+<%= render_if_exists 'layouts/mailer/additional_text' %>
<%= text_footer_message -%>
diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml
index eb148d72da1..f21cf1ad34b 100644
--- a/app/views/notify/closed_issue_email.html.haml
+++ b/app/views/notify/closed_issue_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was closed by #{sanitize_name(@updated_by.name)}
+ Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}.
diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml
index b1f0a3f37ec..5567adc9165 100644
--- a/app/views/notify/closed_issue_email.text.haml
+++ b/app/views/notify/closed_issue_email.text.haml
@@ -1,3 +1,3 @@
-Issue was closed by #{sanitize_name(@updated_by.name)}
+Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}.
Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)}
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 22a721ee9ad..0edd8ee5e46 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -4,6 +4,7 @@
- project = local_assigns.fetch(:project) { @project }
- content_url = local_assigns.fetch(:content_url) { @tree.readme ? project_blob_path(@project, tree_join(@ref, @tree.readme.path)) : project_tree_path(@project, @ref) }
- show_auto_devops_callout = show_auto_devops_callout?(@project)
+- vue_file_list = Feature.enabled?(:vue_file_list, @project)
#tree-holder.tree-holder.clearfix
.nav-block
@@ -13,7 +14,12 @@
= render 'shared/commit_well', commit: commit, ref: ref, project: project
- if is_project_overview
- .project-buttons.append-bottom-default
+ .project-buttons.append-bottom-default{ class: ("js-hide-on-navigation" if vue_file_list) }
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
- = render 'projects/tree/tree_content', tree: @tree, content_url: content_url
+ - if vue_file_list
+ #js-tree-list{ data: { project_path: @project.full_path, ref: ref } }
+ - if @tree.readme
+ = render "projects/tree/readme", readme: @tree.readme
+ - else
+ = render 'projects/tree/tree_content', tree: @tree, content_url: content_url
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index b72f0e39b23..b2dab0b5348 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -7,3 +7,4 @@
= render 'shared/no_password'
- unless project.empty_repo?
= render 'shared/auto_devops_implicitly_enabled_banner', project: project
+ = render_if_exists 'projects/above_size_limit_warning', project: project
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 3ca4abddbb8..a97322dace4 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,7 +1,7 @@
- empty_repo = @project.empty_repo?
- show_auto_devops_callout = show_auto_devops_callout?(@project)
- max_project_topic_length = 15
-.project-home-panel{ class: ("empty-project" if empty_repo) }
+.project-home-panel{ class: [("empty-project" if empty_repo), ("js-hide-on-navigation" if Feature.enabled?(:vue_file_list, @project))] }
.row.append-bottom-8
.home-panel-title-row.col-md-12.col-lg-6.d-flex
.avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 91c51d5e091..0e15f581ddc 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -10,7 +10,7 @@
.branch-info
.branch-title
= sprite_icon('fork', size: 12)
- = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8' do
+ = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8 qa-branch-name' do
= branch.name
- if branch.name == @repository.root_ref
%span.badge.badge-primary.prepend-left-5 default
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 43f1cd01b67..d270e461ac8 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -1,5 +1,6 @@
- @no_container = true
- page_title _('Branches')
+- add_to_breadcrumbs(_('Repository'), project_tree_path(@project))
%div{ class: container_class }
.top-area.adjust
@@ -44,6 +45,8 @@
= link_to new_project_branch_path(@project), class: 'btn btn-success' do
= s_('Branches|New branch')
+ = render_if_exists 'projects/commits/mirror_status'
+
- if can?(current_user, :admin_project, @project)
- project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
.row-content-block
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 9d254463fb6..2db1efdd52f 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -30,6 +30,8 @@
= link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
= icon("rss")
+ = render_if_exists 'projects/commits/mirror_status'
+
%div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list
= render 'commits', project: @project, ref: @ref
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 73e2a4ffb8b..e68fa5d08c7 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -29,7 +29,7 @@
.form-check.append-bottom-10
= check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input'
= label_tag :only_protected_branches, _('Only mirror protected branches'), class: 'form-check-label'
- = link_to icon('question-circle'), help_page_path('user/project/protected_branches')
+ = link_to icon('question-circle'), help_page_path('user/project/protected_branches'), target: '_blank'
.panel-footer
= f.submit _('Mirror repository'), class: 'btn btn-success js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 0590578c3fe..efabb7f7b19 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -19,4 +19,5 @@
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
= f.submit _("Add to project"), class: "btn btn-success qa-add-member-button"
- = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project")
+ - if can_import_members?
+ = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project")
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index b12ae995ece..366d7a7a2eb 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -1,2 +1,2 @@
= render layout: 'projects/protected_branches/shared/protected_branch', locals: { protected_branch: protected_branch } do
- = render partial: 'projects/protected_branches/update_protected_branch', locals: { protected_branch: protected_branch }
+ = render_if_exists 'projects/protected_branches/update_protected_branch', protected_branch: protected_branch
diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml
index 380430ff52b..520f342f567 100644
--- a/app/views/projects/settings/_general.html.haml
+++ b/app/views/projects/settings/_general.html.haml
@@ -27,7 +27,7 @@
.row= render_if_exists 'projects/classification_policy_settings', f: f
- .row= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project
+ = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project
.form-group.prepend-top-default.append-bottom-20
.avatar-container.s90
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index 4daacbe157c..e935af23659 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,5 +1,5 @@
- if readme.rich_viewer
- %article.file-holder.readme-holder{ id: 'readme', class: ("limited-width-container" unless fluid_layout) }
+ %article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-hide-on-navigation" if Feature.enabled?(:vue_file_list, @project))] }
.js-file-title.file-title
= blob_icon readme.mode, readme.name
= link_to project_blob_path(@project, tree_join(@ref, readme.path)) do
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index a2df0347fd6..1e509ea0d1f 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -16,7 +16,12 @@
= ssh_clone_button(project)
%li
= http_clone_button(project)
+ = render_if_exists 'shared/kerberos_clone_button', project: project
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
.input-group-append
= clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
+
+ = render_if_exists 'shared/geo_modal_button'
+
+= render_if_exists 'shared/geo_modal', project: project
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 2c185549b24..63557c882f4 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -164,7 +164,7 @@
= dropdown_content
= dropdown_loading
= dropdown_footer add_content_class: true do
- %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true }
+ %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ type: 'button', disabled: true }
= _('Move')
= icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 2e5747121b6..2db1f67a793 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -53,7 +53,7 @@
= time_ago_with_tooltip(member.created_at)
- if show_roles
- current_resource = @project || @group
- .controls.member-controls.row
+ .controls.member-controls
- if show_controls && member.source == current_resource
- if member.can_resend_invite?
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 01acbf8eadd..3191eaa1e2c 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -9,7 +9,7 @@
%i.fa.fa-clock-o
= event.created_at.to_time.in_time_zone.strftime('%-I:%M%P')
- if event.visible_to_user?(current_user)
- - if event.push?
+ - if event.push_action?
#{event.action_name} #{event.ref_type}
%strong
- commits_path = project_commits_path(event.project, event.ref_name)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 211e3eafac6..6dc61088e65 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -45,7 +45,7 @@
= emoji_icon(@user.status.emoji)
= markdown_field(@user.status, :message)
- .cover-desc.member-date
+ .cover-desc.member-date.cgray
%p
%span.middle-dot-divider
@#{@user.username}
@@ -53,7 +53,7 @@
%span.middle-dot-divider
= s_('Member since %{date}') % { date: @user.created_at.to_date.to_s(:long) }
- .cover-desc
+ .cover-desc.cgray
- unless @user.public_email.blank?
.profile-link-holder.middle-dot-divider
= link_to @user.public_email, "mailto:#{@user.public_email}", class: 'text-link'
@@ -82,7 +82,7 @@
= @user.organization
- if @user.bio.present?
- .cover-desc
+ .cover-desc.cgray
%p.profile-user-bio
= @user.bio
diff --git a/app/workers/cluster_configure_worker.rb b/app/workers/cluster_configure_worker.rb
index 37ea7dde7a1..6f64b7ea0ab 100644
--- a/app/workers/cluster_configure_worker.rb
+++ b/app/workers/cluster_configure_worker.rb
@@ -6,7 +6,7 @@ class ClusterConfigureWorker
def perform(cluster_id)
Clusters::Cluster.managed.find_by_id(cluster_id).try do |cluster|
- if cluster.project_type? || Feature.disabled?(:ci_preparing_state, default_enabled: true)
+ if cluster.project_type?
Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster)
end
end
diff --git a/app/workers/pages_domain_removal_cron_worker.rb b/app/workers/pages_domain_removal_cron_worker.rb
index 3aca123e5ac..79f38e1b89f 100644
--- a/app/workers/pages_domain_removal_cron_worker.rb
+++ b/app/workers/pages_domain_removal_cron_worker.rb
@@ -5,8 +5,6 @@ class PagesDomainRemovalCronWorker
include CronjobQueue
def perform
- return unless Feature.enabled?(:remove_disabled_domains)
-
PagesDomain.for_removal.find_each do |domain|
domain.destroy!
rescue => e
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 29a7f8e691a..3efb5343a96 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -48,7 +48,7 @@ class ProcessCommitWorker
# Issues::CloseService#execute.
IssueCollection.new(issues).updatable_by_user(user).each do |issue|
Issues::CloseService.new(project, author)
- .close_issue(issue, commit: commit)
+ .close_issue(issue, closed_via: commit)
end
end
diff --git a/changelogs/unreleased/1340-request-job-with-age.yml b/changelogs/unreleased/1340-request-job-with-age.yml
new file mode 100644
index 00000000000..766ac008c2e
--- /dev/null
+++ b/changelogs/unreleased/1340-request-job-with-age.yml
@@ -0,0 +1,5 @@
+---
+title: "Added option to filter jobs by age in the /job/request API endpoint."
+merge_request: 1340
+author: Dmitry Chepurovskiy
+type: added
diff --git a/changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml b/changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml
new file mode 100644
index 00000000000..bb2fc9af2a1
--- /dev/null
+++ b/changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Include information if issue was clossed via merge request or commit
+merge_request: 15610
+author: Michał Zając
+type: changed
diff --git a/changelogs/unreleased/237-style-toast-component.yml b/changelogs/unreleased/237-style-toast-component.yml
new file mode 100644
index 00000000000..2420df0ee55
--- /dev/null
+++ b/changelogs/unreleased/237-style-toast-component.yml
@@ -0,0 +1,5 @@
+---
+title: Style the toast component according to design specs.
+merge_request: 27734
+author:
+type: added
diff --git a/changelogs/unreleased/49517-fix-notes-import-export.yml b/changelogs/unreleased/49517-fix-notes-import-export.yml
new file mode 100644
index 00000000000..a9f4d736e0b
--- /dev/null
+++ b/changelogs/unreleased/49517-fix-notes-import-export.yml
@@ -0,0 +1,5 @@
+---
+title: Fix diff notes and discussion notes being exported as regular notes
+merge_request: 28401
+author:
+type: fixed
diff --git a/changelogs/unreleased/49915-fix-error-500-admin-projects-nil-storage.yml b/changelogs/unreleased/49915-fix-error-500-admin-projects-nil-storage.yml
new file mode 100644
index 00000000000..307c2bfb49d
--- /dev/null
+++ b/changelogs/unreleased/49915-fix-error-500-admin-projects-nil-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Fix an error in projects admin when statistics are missing
+merge_request: 28355
+author:
+type: fixed
diff --git a/changelogs/unreleased/60379-remove-ci-preparing-state-feature-flag.yml b/changelogs/unreleased/60379-remove-ci-preparing-state-feature-flag.yml
new file mode 100644
index 00000000000..a9b7aeb3024
--- /dev/null
+++ b/changelogs/unreleased/60379-remove-ci-preparing-state-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove ability for group clusters to be automatically configured on creation
+merge_request: 27245
+author:
+type: removed
diff --git a/changelogs/unreleased/60818_yamllint_project_root.yml b/changelogs/unreleased/60818_yamllint_project_root.yml
new file mode 100644
index 00000000000..b34a50e6a9c
--- /dev/null
+++ b/changelogs/unreleased/60818_yamllint_project_root.yml
@@ -0,0 +1,5 @@
+---
+title: Fix yaml linting for project root *.yml files
+merge_request: 27579
+author: Will Hall
+type: fixed
diff --git a/changelogs/unreleased/61606-support-string-piwik-website-ids.yml b/changelogs/unreleased/61606-support-string-piwik-website-ids.yml
new file mode 100644
index 00000000000..5c525294132
--- /dev/null
+++ b/changelogs/unreleased/61606-support-string-piwik-website-ids.yml
@@ -0,0 +1,5 @@
+---
+title: "Supports Matomo/Piwik string website ID (\"Protect Track ID\" plugin)"
+merge_request: 28214
+author: DUVERGIER Claude
+type: fixed \ No newline at end of file
diff --git a/changelogs/unreleased/61629-dependency-installation-error-on-fsevents-1-2-4-with-node-js-12.yml b/changelogs/unreleased/61629-dependency-installation-error-on-fsevents-1-2-4-with-node-js-12.yml
new file mode 100644
index 00000000000..bbe43760953
--- /dev/null
+++ b/changelogs/unreleased/61629-dependency-installation-error-on-fsevents-1-2-4-with-node-js-12.yml
@@ -0,0 +1,5 @@
+---
+title: Update indirect dependency fsevents from 1.2.4 to 1.2.9
+merge_request: 28220
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/61795-fix-error-when-moving-issues.yml b/changelogs/unreleased/61795-fix-error-when-moving-issues.yml
new file mode 100644
index 00000000000..6812baa07c3
--- /dev/null
+++ b/changelogs/unreleased/61795-fix-error-when-moving-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unintended error message shown when moving issues
+merge_request: 28317
+author:
+type: fixed
diff --git a/changelogs/unreleased/61914-fix-emojis-urls.yml b/changelogs/unreleased/61914-fix-emojis-urls.yml
new file mode 100644
index 00000000000..578edf4a063
--- /dev/null
+++ b/changelogs/unreleased/61914-fix-emojis-urls.yml
@@ -0,0 +1,5 @@
+---
+title: Fix emojis URLs
+merge_request: 28371
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-branch-to-project-search-api.yml b/changelogs/unreleased/add-branch-to-project-search-api.yml
new file mode 100644
index 00000000000..74cff94ab76
--- /dev/null
+++ b/changelogs/unreleased/add-branch-to-project-search-api.yml
@@ -0,0 +1,5 @@
+---
+title: Added ref querystring parameter to project search API to allow searching on branches/tags other than the default
+merge_request: 28069
+author: Lee Tickett
+type: added
diff --git a/changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml b/changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml
new file mode 100644
index 00000000000..c2dcd309abd
--- /dev/null
+++ b/changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml
@@ -0,0 +1,5 @@
+---
+title: Remove non-semantic use of `.row` in member listing controls
+merge_request: 28204
+author:
+type: fixed
diff --git a/changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml b/changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml
new file mode 100644
index 00000000000..a626193dc27
--- /dev/null
+++ b/changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml
@@ -0,0 +1,5 @@
+---
+title: Add issues_statistics api endpoints and extend issues search api
+merge_request: 27366
+author:
+type: added
diff --git a/changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml b/changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml
new file mode 100644
index 00000000000..332105bb269
--- /dev/null
+++ b/changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml
@@ -0,0 +1,5 @@
+---
+title: Fix flyout nav on small viewports
+merge_request: 25998
+author:
+type: fixed
diff --git a/changelogs/unreleased/diff-whitespace-setting-changes.yml b/changelogs/unreleased/diff-whitespace-setting-changes.yml
new file mode 100644
index 00000000000..640e9e589df
--- /dev/null
+++ b/changelogs/unreleased/diff-whitespace-setting-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed show whitespace button not refetching diff content
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/display-junit-classname-in-modal.yml b/changelogs/unreleased/display-junit-classname-in-modal.yml
new file mode 100644
index 00000000000..c5140456e4e
--- /dev/null
+++ b/changelogs/unreleased/display-junit-classname-in-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Display classname JUnit attribute in report modal
+merge_request: 28376
+author:
+type: added
diff --git a/changelogs/unreleased/dz-patch-58.yml b/changelogs/unreleased/dz-patch-58.yml
new file mode 100644
index 00000000000..97ceadd303d
--- /dev/null
+++ b/changelogs/unreleased/dz-patch-58.yml
@@ -0,0 +1,5 @@
+---
+title: Replace Oxygen-Sans font with Noto Sans
+merge_request: 28322
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-db-migrate-is-failed-on-mysql8.yml b/changelogs/unreleased/fix-db-migrate-is-failed-on-mysql8.yml
new file mode 100644
index 00000000000..63f134808e3
--- /dev/null
+++ b/changelogs/unreleased/fix-db-migrate-is-failed-on-mysql8.yml
@@ -0,0 +1,5 @@
+---
+title: Fix. `db:migrate` is failed on MySQL 8
+merge_request: 28351
+author: sue445
+type: fixed
diff --git a/changelogs/unreleased/fix-project-visibility-level-validation.yml b/changelogs/unreleased/fix-project-visibility-level-validation.yml
new file mode 100644
index 00000000000..9581a475842
--- /dev/null
+++ b/changelogs/unreleased/fix-project-visibility-level-validation.yml
@@ -0,0 +1,5 @@
+---
+title: Fix project visibility level validation
+merge_request: 28305
+author: Peter Marko
+type: fixed
diff --git a/changelogs/unreleased/fix-too-many-loops-cron-error.yml b/changelogs/unreleased/fix-too-many-loops-cron-error.yml
new file mode 100644
index 00000000000..a9b5b761439
--- /dev/null
+++ b/changelogs/unreleased/fix-too-many-loops-cron-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix "too many loops" error by handling gracefully cron schedules for non existent days
+merge_request: 28002
+author:
+type: fixed
diff --git a/changelogs/unreleased/fl-fix-next-flag-for-good.yml b/changelogs/unreleased/fl-fix-next-flag-for-good.yml
new file mode 100644
index 00000000000..93f27824213
--- /dev/null
+++ b/changelogs/unreleased/fl-fix-next-flag-for-good.yml
@@ -0,0 +1,5 @@
+---
+title: Next badge must visible when canary flag is true
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/include-ee-fixtures.yml b/changelogs/unreleased/include-ee-fixtures.yml
new file mode 100644
index 00000000000..ba500d92de3
--- /dev/null
+++ b/changelogs/unreleased/include-ee-fixtures.yml
@@ -0,0 +1,5 @@
+---
+title: Add EE fixtures to SeedFu list
+merge_request: 28241
+author:
+type: other
diff --git a/changelogs/unreleased/jc-omit-count-diverging-commits-max.yml b/changelogs/unreleased/jc-omit-count-diverging-commits-max.yml
new file mode 100644
index 00000000000..23235060a98
--- /dev/null
+++ b/changelogs/unreleased/jc-omit-count-diverging-commits-max.yml
@@ -0,0 +1,5 @@
+---
+title: Omit max-count for diverging_commit_counts behind feature flag
+merge_request: 28157
+author:
+type: other
diff --git a/changelogs/unreleased/kinolaev-master-patch-13154.yml b/changelogs/unreleased/kinolaev-master-patch-13154.yml
new file mode 100644
index 00000000000..3292ff797e2
--- /dev/null
+++ b/changelogs/unreleased/kinolaev-master-patch-13154.yml
@@ -0,0 +1,5 @@
+---
+title: 'Auto-DevOps: allow to disable rollout status check'
+merge_request: 28130
+author: Sergej Nikolaev <kinolaev@gmail.com>
+type: fixed
diff --git a/changelogs/unreleased/leipert-node-12-compatibility.yml b/changelogs/unreleased/leipert-node-12-compatibility.yml
new file mode 100644
index 00000000000..18025d33a6d
--- /dev/null
+++ b/changelogs/unreleased/leipert-node-12-compatibility.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade dependencies for node 12 compatibility
+merge_request: 28323
+author:
+type: fixed
diff --git a/changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml b/changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml
new file mode 100644
index 00000000000..daeefd3ffd7
--- /dev/null
+++ b/changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of users autocomplete when there are lots of results
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/mm12935.yml b/changelogs/unreleased/mm12935.yml
new file mode 100644
index 00000000000..782586c514e
--- /dev/null
+++ b/changelogs/unreleased/mm12935.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Content-Type override for Mattermost OAuth login
+merge_request:
+author: Harrison Healey
+type: removed
diff --git a/changelogs/unreleased/sh-fix-personal-snippet-uploads-object-storage.yml b/changelogs/unreleased/sh-fix-personal-snippet-uploads-object-storage.yml
new file mode 100644
index 00000000000..603afa8573f
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-personal-snippet-uploads-object-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect prefix used in new uploads for personal snippets
+merge_request: 28337
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-rebase-error-clearing.yml b/changelogs/unreleased/sh-fix-rebase-error-clearing.yml
new file mode 100644
index 00000000000..4f5f2779e7f
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-rebase-error-clearing.yml
@@ -0,0 +1,5 @@
+---
+title: Properly clear the merge error upon rebase failure
+merge_request: 28319
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-tag-push-remote-mirror.yml b/changelogs/unreleased/sh-fix-tag-push-remote-mirror.yml
new file mode 100644
index 00000000000..7f33ab28e3d
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-tag-push-remote-mirror.yml
@@ -0,0 +1,5 @@
+---
+title: Fix remote mirrors not updating after tag push
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-update-process-mem.yml b/changelogs/unreleased/sh-update-process-mem.yml
new file mode 100644
index 00000000000..51b22fb0f00
--- /dev/null
+++ b/changelogs/unreleased/sh-update-process-mem.yml
@@ -0,0 +1,5 @@
+---
+title: Update get_process_mem to 0.2.3
+merge_request: 28248
+author:
+type: other
diff --git a/changelogs/unreleased/update-gitaly-to-v1-42-1.yml b/changelogs/unreleased/update-gitaly-to-v1-42-1.yml
new file mode 100644
index 00000000000..ff42bdd9c0b
--- /dev/null
+++ b/changelogs/unreleased/update-gitaly-to-v1-42-1.yml
@@ -0,0 +1,5 @@
+---
+title: "Update Gitaly to v1.42.1"
+merge_request: 28425
+author:
+type: other
diff --git a/changelogs/unreleased/zj-usage-ping-pool-repository.yml b/changelogs/unreleased/zj-usage-ping-pool-repository.yml
new file mode 100644
index 00000000000..62044a933d0
--- /dev/null
+++ b/changelogs/unreleased/zj-usage-ping-pool-repository.yml
@@ -0,0 +1,5 @@
+---
+title: Add Pool repository to the usage ping
+merge_request: 28267
+author:
+type: other
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 3461099253a..e7166882eea 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -11,7 +11,7 @@ Rails.application.configure do
# and recreated between test runs. Don't rely on the data there!
# Enabling caching of classes slows start-up time because all controllers
- # are loaded at initalization, but it reduces memory and load because files
+ # are loaded at initialization, but it reduces memory and load because files
# are not reloaded with every request. For example, caching is not necessary
# for loading database migrations but useful for handling Knapsack specs.
config.cache_classes = ENV['CACHE_CLASSES'] == 'true'
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index bff809b7661..23377b43f78 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -245,7 +245,7 @@ production: &base
host: example.com
port: 80 # Set to 443 if you serve the pages with HTTPS
https: false # Set to true if you serve the pages with HTTPS
- artifacts_server: true
+ artifacts_server: true # Set to false if you want to disable online view of HTML artifacts
# external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages
# external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
admin:
diff --git a/config/initializers/action_dispatch_http_mime_negotiation.rb b/config/initializers/action_dispatch_http_mime_negotiation.rb
index bdf5b0babfb..6c31de2de55 100644
--- a/config/initializers/action_dispatch_http_mime_negotiation.rb
+++ b/config/initializers/action_dispatch_http_mime_negotiation.rb
@@ -2,7 +2,7 @@
# the extension of the full URL path if no explicit `format` param or `Accept`
# header is provided, like when simply browsing to a page in your browser.
#
-# This is undesireable in GitLab, because many of our paths will end in a ref or
+# This is undesirable in GitLab, because many of our paths will end in a ref or
# blob name that can end with any extension, while these pages should still be
# presented as HTML unless otherwise specified.
diff --git a/config/initializers/seed_fu.rb b/config/initializers/seed_fu.rb
new file mode 100644
index 00000000000..2e48e41a311
--- /dev/null
+++ b/config/initializers/seed_fu.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+if Gitlab.ee?
+ SeedFu.fixture_paths += %W[ee/db/fixtures ee/db/fixtures/#{Rails.env}]
+end
diff --git a/config/karma.config.js b/config/karma.config.js
index 83ba46345f2..b2fc3a32816 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -107,6 +107,8 @@ module.exports = function(config) {
// chrome cannot run in sandboxed mode inside a docker container unless it is run with
// escalated kernel privileges (e.g. docker run --cap-add=CAP_SYS_ADMIN)
'--no-sandbox',
+ // https://bugs.chromium.org/p/chromedriver/issues/detail?id=2870
+ '--enable-features=NetworkService,NetworkServiceInProcess',
],
},
},
diff --git a/config/puma.rb.example b/config/puma.rb.example
new file mode 100644
index 00000000000..6558dbc6cfe
--- /dev/null
+++ b/config/puma.rb.example
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+# Load "path" as a rackup file.
+#
+# The default is "config.ru".
+#
+rackup 'config.ru'
+pidfile '/home/git/gitlab/tmp/pids/puma.pid'
+state_path '/home/git/gitlab/tmp/pids/puma.state'
+
+stdout_redirect '/home/git/gitlab/log/puma.stdout.log',
+ '/home/git/gitlab/log/puma.stderr.log',
+ true
+
+# Configure "min" to be the minimum number of threads to use to answer
+# requests and "max" the maximum.
+#
+# The default is "0, 16".
+#
+threads 1, 16
+
+# By default, workers accept all requests and queue them to pass to handlers.
+# When false, workers accept the number of simultaneous requests configured.
+#
+# Queueing requests generally improves performance, but can cause deadlocks if
+# the app is waiting on a request to itself. See https://github.com/puma/puma/issues/612
+#
+# When set to false this may require a reverse proxy to handle slow clients and
+# queue requests before they reach puma. This is due to disabling HTTP keepalive
+queue_requests false
+
+# Bind the server to "url". "tcp://", "unix://" and "ssl://" are the only
+# accepted protocols.
+bind 'unix:///home/git/gitlab/tmp/sockets/gitlab.socket'
+
+workers 3
+
+require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
+require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer"
+
+on_restart do
+ # Signal application hooks that we're about to restart
+ Gitlab::Cluster::LifecycleEvents.do_master_restart
+end
+
+before_fork do
+ # Signal to the puma killer
+ Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options unless ENV['DISABLE_PUMA_WORKER_KILLER']
+
+ # Signal application hooks that we're about to fork
+ Gitlab::Cluster::LifecycleEvents.do_before_fork
+end
+
+Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options
+on_worker_boot do
+ # Signal application hooks of worker start
+ Gitlab::Cluster::LifecycleEvents.do_worker_start
+end
+
+# Preload the application before starting the workers; this conflicts with
+# phased restart feature. (off by default)
+preload_app!
+
+tag 'gitlab-puma-worker'
+
+# Verifies that all workers have checked in to the master process within
+# the given timeout. If not the worker process will be restarted. Default
+# value is 60 seconds.
+#
+worker_timeout 60
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 90d7f4a04d4..bc19219a0b8 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -111,6 +111,7 @@ namespace :admin do
put :reset_health_check_token
put :clear_repository_check_states
get :integrations, :repository, :templates, :ci_cd, :reporting, :metrics_and_profiling, :network, :geo, :preferences
+ get :lets_encrypt_terms_of_service
end
resources :labels
diff --git a/config/unicorn.rb.example.development b/config/unicorn.rb.example.development
index f7541bb9d55..ae3dc2e37e1 100644
--- a/config/unicorn.rb.example.development
+++ b/config/unicorn.rb.example.development
@@ -58,4 +58,3 @@ after_fork do |server, worker|
# addr = "127.0.0.1:#{9293 + worker.nr}"
# server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true)
end
-
diff --git a/db/migrate/20150509180749_convert_legacy_reference_notes.rb b/db/migrate/20150509180749_convert_legacy_reference_notes.rb
index a44a908c2f5..84d4eb9e51f 100644
--- a/db/migrate/20150509180749_convert_legacy_reference_notes.rb
+++ b/db/migrate/20150509180749_convert_legacy_reference_notes.rb
@@ -7,7 +7,8 @@
# mentioned in 54f7727c850972f0401c1312a7c4a6a380de5666
class ConvertLegacyReferenceNotes < ActiveRecord::Migration[4.2]
def up
- execute %q{UPDATE notes SET note = trim(both '_' from note) WHERE system = true AND note LIKE '\_%\_'}
+ quoted_column_name = ActiveRecord::Base.connection.quote_column_name('system')
+ execute %Q{UPDATE notes SET note = trim(both '_' from note) WHERE #{quoted_column_name} = true AND note LIKE '\_%\_'}
end
def down
diff --git a/db/migrate/20170330141723_disable_invalid_service_templates2.rb b/db/migrate/20170330141723_disable_invalid_service_templates2.rb
index 91ec19dfa87..f09f3b3e355 100644
--- a/db/migrate/20170330141723_disable_invalid_service_templates2.rb
+++ b/db/migrate/20170330141723_disable_invalid_service_templates2.rb
@@ -1,5 +1,5 @@
# This is the same as DisableInvalidServiceTemplates. Later migrations may have
-# inadventently enabled some invalid templates again.
+# inadvertently enabled some invalid templates again.
#
class DisableInvalidServiceTemplates2 < ActiveRecord::Migration[4.2]
DOWNTIME = false
diff --git a/db/migrate/20190515125613_add_application_settings_elasticsearch_shards.rb b/db/migrate/20190515125613_add_application_settings_elasticsearch_shards.rb
new file mode 100644
index 00000000000..9cebc0f8db4
--- /dev/null
+++ b/db/migrate/20190515125613_add_application_settings_elasticsearch_shards.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddApplicationSettingsElasticsearchShards < ActiveRecord::Migration[5.1]
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :elasticsearch_shards, :integer, null: false, default: 5
+ add_column :application_settings, :elasticsearch_replicas, :integer, null: false, default: 1
+ end
+end
diff --git a/db/migrate/20190516011213_add_build_queued_at_index.rb b/db/migrate/20190516011213_add_build_queued_at_index.rb
new file mode 100644
index 00000000000..77ffa7cd4e9
--- /dev/null
+++ b/db/migrate/20190516011213_add_build_queued_at_index.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# This migration make queued_at field indexed to speed up builds filtering by job_age
+
+class AddBuildQueuedAtIndex < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, :queued_at
+ end
+
+ def down
+ remove_concurrent_index :ci_builds, :queued_at
+ end
+end
diff --git a/db/post_migrate/20190301081611_migrate_project_migrate_sidekiq_queue.rb b/db/post_migrate/20190301081611_migrate_project_migrate_sidekiq_queue.rb
index 6af7902e0c4..46108d142b5 100644
--- a/db/post_migrate/20190301081611_migrate_project_migrate_sidekiq_queue.rb
+++ b/db/post_migrate/20190301081611_migrate_project_migrate_sidekiq_queue.rb
@@ -5,8 +5,6 @@ class MigrateProjectMigrateSidekiqQueue < ActiveRecord::Migration[5.0]
DOWNTIME = false
- DOWNTIME = false
-
def up
sidekiq_queue_migrate 'project_migrate_hashed_storage', to: 'hashed_storage:hashed_storage_project_migrate'
end
diff --git a/db/post_migrate/20190424134256_drop_projects_ci_id.rb b/db/post_migrate/20190424134256_drop_projects_ci_id.rb
index 79fa9704f1f..44e8f316393 100644
--- a/db/post_migrate/20190424134256_drop_projects_ci_id.rb
+++ b/db/post_migrate/20190424134256_drop_projects_ci_id.rb
@@ -22,7 +22,12 @@ class DropProjectsCiId < ActiveRecord::Migration[5.1]
end
def down
- add_column :projects, :ci_id, :integer
- add_concurrent_index :projects, :ci_id
+ unless column_exists?(:projects, :ci_id)
+ add_column :projects, :ci_id, :integer
+ end
+
+ unless index_exists?(:projects, :ci_id)
+ add_concurrent_index :projects, :ci_id
+ end
end
end
diff --git a/db/schema.rb b/db/schema.rb
index 159e7e03cf4..412b5313b69 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20190506135400) do
+ActiveRecord::Schema.define(version: 20190516011213) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -189,6 +189,8 @@ ActiveRecord::Schema.define(version: 20190506135400) do
t.string "encrypted_external_auth_client_key_pass_iv"
t.string "lets_encrypt_notification_email"
t.boolean "lets_encrypt_terms_of_service_accepted", default: false, null: false
+ t.integer "elasticsearch_shards", default: 5, null: false
+ t.integer "elasticsearch_replicas", default: 1, null: false
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
end
@@ -378,6 +380,7 @@ ActiveRecord::Schema.define(version: 20190506135400) do
t.index ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree
t.index ["project_id", "status"], name: "index_ci_builds_project_id_and_status_for_live_jobs_partial2", where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))", using: :btree
t.index ["protected"], name: "index_ci_builds_on_protected", using: :btree
+ t.index ["queued_at"], name: "index_ci_builds_on_queued_at", using: :btree
t.index ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
t.index ["scheduled_at"], name: "partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs", where: "((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text))", using: :btree
t.index ["stage_id", "stage_idx"], name: "tmp_build_stage_position_index", where: "(stage_idx IS NOT NULL)", using: :btree
diff --git a/doc/README.md b/doc/README.md
index dd4909ce303..3863e17c268 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -4,7 +4,7 @@ description: 'Learn how to use and administer GitLab, the most scalable Git-base
---
<div class="display-none">
- <em>Visit <a href="https://docs.gitlab.com/ce/">docs.gitlab.com</a> for optimized
+ <em>Visit <a href="https://docs.gitlab.com/ee/">docs.gitlab.com</a> for optimized
navigation, discoverability, and readability.</em>
</div>
<!-- the div above will not display on the docs site but will display on /help -->
@@ -107,13 +107,18 @@ The following documentation relates to the DevOps **Plan** stage:
| Plan Topics | Description |
|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------|
+| [Burndown Charts](user/project/milestones/burndown_charts.md) **[STARTER]** | Watch your project's progress throughout a specific milestone. |
| [Discussions](user/discussions/index.md) | Threads, comments, and resolvable discussions in issues, commits, and merge requests. |
| [Due Dates](user/project/issues/due_dates.md) | Keep track of issue deadlines. |
-| [Quick Actions](user/project/quick_actions.md) | Shortcuts for common actions on issues or merge requests, replacing the need to click buttons or use dropdowns in GitLab's UI. |
+| [Epics](user/group/epics/index.md) **[ULTIMATE]** | Tracking groups of issues that share a theme. |
| [Issues](user/project/issues/index.md), including [confidential issues](user/project/issues/confidential_issues.md),<br/>[issue and merge request templates](user/project/description_templates.md),<br/>and [moving issues](user/project/issues/moving_issues.md) | Project issues, restricting access to issues, create templates for submitting new issues and merge requests, and moving issues between projects. |
| [Labels](user/project/labels.md) | Categorize issues or merge requests with descriptive labels. |
| [Milestones](user/project/milestones/index.md) | Set milestones for delivery of issues and merge requests, with optional due date. |
| [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. |
+| [Quick Actions](user/project/quick_actions.md) | Shortcuts for common actions on issues or merge requests, replacing the need to click buttons or use dropdowns in GitLab's UI. |
+| [Related Issues](user/project/issues/related_issues.md) **[STARTER]** | Create a relationship between issues. |
+| [Roadmap](user/group/roadmap/index.md) **[ULTIMATE]** | Visualize epic timelines. |
+| [Service Desk](user/project/service_desk.md) **[PREMIUM]** | A simple way to allow people to create issues in your GitLab instance without needing their own user account. |
| [Time Tracking](workflow/time_tracking.md) | Track time spent on issues and merge requests. |
| [Todos](workflow/todos.md) | Keep track of work requiring attention with a chronological list displayed on a simple dashboard. |
@@ -136,16 +141,21 @@ The following documentation relates to the DevOps **Create** stage:
#### Projects and Groups
-| Create Topics - Projects and Groups | Description |
-|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------|
-| [Create](gitlab-basics/create-project.md) and [fork](gitlab-basics/fork-project.md) projects, and<br/>[import and export<br/>projects between instances](user/project/settings/import_export.md) | Create, duplicate, and move projects. |
-| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy your static website with GitLab Pages. |
-| [Groups](user/group/index.md) and [Subgroups](user/group/subgroups/index.md) | Organize your projects in groups. |
-| [Projects](user/project/index.md), including [project access](public_access/public_access.md)<br/>and [settings](user/project/settings/index.md) | Host source code, and control your project's visibility and set configuration. |
-| [Search through GitLab](user/search/index.md) | Search for issues, merge requests, projects, groups, and todos. |
-| [Snippets](user/snippets.md) | Snippets allow you to create little bits of code. |
-| [Web IDE](user/project/web_ide/index.md) | Edit files within GitLab's user interface. |
-| [Wikis](user/project/wiki/index.md) | Enhance your repository documentation with built-in wikis. |
+| Create Topics - Projects and Groups | Description |
+|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------|
+| [Advanced global search](user/search/advanced_global_search.md) **[STARTER]** | Leverage Elasticsearch for faster, more advanced code search across your entire GitLab instance. |
+| [Advanced syntax search](user/search/advanced_search_syntax.md) **[STARTER]** | Use advanced queries for more targeted search results. |
+| [Contribution analytics](user/group/contribution_analytics/index.md) **[STARTER]** | See detailed statistics of group contributors. |
+| [Create](gitlab-basics/create-project.md) and [fork](gitlab-basics/fork-project.md) projects, and<br/>[import and export projects<br/>between instances](user/project/settings/import_export.md) | Create, duplicate, and move projects. |
+| [File locking](user/project/file_lock.md) **[PREMIUM]** | Lock files to avoid merge conflicts. |
+| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy your static website with GitLab Pages. |
+| [Groups](user/group/index.md) and [Subgroups](user/group/subgroups/index.md) | Organize your projects in groups. |
+| [Issues Analytics](user/group/issues_analytics/index.md) **[PREMIUM]** | Check how many issues were created per month. |
+| [Projects](user/project/index.md), including [project access](public_access/public_access.md)<br/>and [settings](user/project/settings/index.md) | Host source code, and control your project's visibility and set configuration. |
+| [Search through GitLab](user/search/index.md) | Search for issues, merge requests, projects, groups, and todos. |
+| [Snippets](user/snippets.md) | Snippets allow you to create little bits of code. |
+| [Web IDE](user/project/web_ide/index.md) | Edit files within GitLab's user interface. |
+| [Wikis](user/project/wiki/index.md) | Enhance your repository documentation with built-in wikis. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -165,7 +175,9 @@ The following documentation relates to the DevOps **Create** stage:
| [Files](user/project/repository/index.md#files) | Files management. |
| [Jupyter Notebook files](user/project/repository/index.md#jupyter-notebook-files) | GitLab's support for `.ipynb` files. |
| [Protected branches](user/project/protected_branches.md) | Use protected branches. |
+| [Push rules](push_rules/push_rules.md) **[STARTER]** | Additional control over pushes to your projects. |
| [Repositories](user/project/repository/index.md) | Manage source code repositories in GitLab's user interface. |
+| [Repository mirroring](workflow/repository_mirroring.md) **[STARTER]** | Push to or pull from repositories outside of GitLab |
| [Start a merge request](user/project/repository/web_editor.md#tips) | Start merge request when committing via GitLab's user interface. |
<div align="right">
@@ -192,13 +204,14 @@ The following documentation relates to the DevOps **Create** stage:
#### Integration and Automation
-| Create Topics - Integration and Automation | Description |
-|:------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------|
-| [GitLab API](api/README.md) | Integrate GitLab via a simple and powerful API. |
-| [GitLab Integration](integration/README.md) | Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication. |
-| [GitLab Webhooks](user/project/integrations/webhooks.md) | Let GitLab notify you when new code has been pushed to your project. |
-| [Project Services](user/project/integrations/project_services.md) | Integrate a project with external services, such as CI and chat. |
-| [Trello Power-Up](integration/trello_power_up.md) | Integrate with GitLab's Trello Power-Up. |
+| Create Topics - Integration and Automation | Description |
+|:------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------|
+| [GitLab API](api/README.md) | Integrate GitLab via a simple and powerful API. |
+| [GitLab Integration](integration/README.md) | Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication. |
+| [GitLab Webhooks](user/project/integrations/webhooks.md) | Let GitLab notify you when new code has been pushed to your project. |
+| [JIRA Development Panel](integration/jira_development_panel.md) **[PREMIUM]** | See GitLab information in the JIRA Development Panel. |
+| [Project Services](user/project/integrations/project_services.md) | Integrate a project with external services, such as CI and chat. |
+| [Trello Power-Up](integration/trello_power_up.md) | Integrate with GitLab's Trello Power-Up. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -218,12 +231,14 @@ scales to run your tests faster.
The following documentation relates to the DevOps **Verify** stage:
-| Verify Topics | Description |
-|:---------------------------------------------------------|:-----------------------------------------------------------------------------|
-| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. |
-| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. |
-| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. |
-| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. |
+| Verify Topics | Description |
+|:----------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------|
+| [Code Quality reports](user/project/merge_requests/code_quality.md) **[STARTER]** | Analyze source code quality. |
+| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Integration with GitLab. |
+| [JUnit test reports](ci/junit_test_reports.md) | Display JUnit test reports on merge requests. |
+| [Multi-project pipelines](ci/multi_project_pipelines.md) **[PREMIUM]** | Visualize entire pipelines that span multiple projects, including all cross-project inter-dependencies. |
+| [Pipeline Graphs](ci/pipelines.md#visualizing-pipelines) | Visualize builds. |
+| [Review Apps](ci/review_apps/index.md) | Preview changes to your application right from a merge request. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -242,6 +257,7 @@ The following documentation relates to the DevOps **Package** stage:
| Package Topics | Description |
|:----------------------------------------------------------------|:-------------------------------------------------------|
| [GitLab Container Registry](user/project/container_registry.md) | Learn how to use GitLab's built-in Container Registry. |
+| [GitLab Packages](administration/packages.md) **[PREMIUM]** | Use GitLab as an NPM registry or Maven repository. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -257,14 +273,17 @@ confidently and securely with GitLab’s built-in Continuous Delivery and Deploy
The following documentation relates to the DevOps **Release** stage:
-| Release Topics | Description |
-|:------------------------------------------------------------|:---------------------------------------------------------------------------------------------|
-| [Auto Deploy](topics/autodevops/index.md#auto-deploy) | Configure GitLab for the deployment of your application. |
-| [Environments and deployments](ci/environments.md) | With environments, you can control the continuous deployment of your software within GitLab. |
-| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Deployment and Delivery with GitLab. |
-| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy a static site directly from GitLab. |
-| [Protected Runners](ci/runners/README.md#protected-runners) | Select Runners to only pick jobs for protected branches and tags. |
-| [Scheduled Pipelines](user/project/pipelines/schedules.md) | Execute pipelines on a schedule. |
+| Release Topics | Description |
+|:------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------|
+| [Auto Deploy](topics/autodevops/index.md#auto-deploy) | Configure GitLab for the deployment of your application. |
+| [Canary Deployments](user/project/canary_deployments.md) **[PREMIUM]** | Employ a popular CI strategy where a small portion of the fleet is updated to the new version first. |
+| [Deploy Boards](user/project/deploy_boards.md) **[PREMIUM]** | View the current health and status of each CI environment running on Kubernetes, displaying the status of the pods in the deployment. |
+| [Environments and deployments](ci/environments.md) | With environments, you can control the continuous deployment of your software within GitLab. |
+| [Environment-specific variables](ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium) **[PREMIUM]** | Limit scope of variables to specific environments. |
+| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Deployment and Delivery with GitLab. |
+| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy a static site directly from GitLab. |
+| [Protected Runners](ci/runners/README.md#protected-runners) | Select Runners to only pick jobs for protected branches and tags. |
+| [Scheduled Pipelines](user/project/pipelines/schedules.md) | Execute pipelines on a schedule. |
<div align="right">
<a type="button" class="btn btn-default" href="#overview">
@@ -288,6 +307,7 @@ The following documentation relates to the DevOps **Configure** stage:
| [GitLab ChatOps](ci/chatops/README.md) | Interact with CI/CD jobs through chat services. |
| [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. |
| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. |
+| [Multiple Kubernetes Clusters](user/project/clusters/index.md#multiple-kubernetes-clusters-premium) **[PREMIUM]** | Associate more than one Kubernetes clusters to your project. |
| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. |
| [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. |
| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. |
@@ -323,16 +343,23 @@ The following documentation relates to the DevOps **Monitor** stage:
### Secure
-GitLab can help you secure your applications from within your development lifecycle.
+Check your application for security vulnerabilities that may lead to unauthorized access,
+data leaks, and denial of services. GitLab will perform static and dynamic tests on the
+code of your application, looking for known flaws and report them in the merge request
+so you can fix them before merging. Security teams can use dashboards to get a
+high-level view on projects and groups, and start remediation processes when needed.
The following documentation relates to the DevOps **Secure** stage:
-| Monitor Topics | Description |
-|:----------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------|
-| [Container Scanning example](ci/examples/container_scanning.md) | `.gitlab-ci.yml` example of using Clair and clair-scanner to scan docker images for known vulnerabilities. |
-
-NOTE: **Note:**
-Viewing [Container Scanning reports](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html) within merge requests requires [GitLab Ultimate](https://about.gitlab.com/pricing/).
+| Secure Topics | Description |
+|:------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------|
+| [Container Scanning](user/application_security/container_scanning/index.md) **[ULTIMATE]** | Use Clair to scan docker images for known vulnerabilities. |
+| [Dependency Scanning](user/application_security/dependency_scanning/index.md) **[ULTIMATE]** | Analyze your dependencies for known vulnerabilities. |
+| [Dynamic Application Security Testing (DAST)](user/application_security/dast/index.md) **[ULTIMATE]** | Analyze running web applications for known vulnerabilities. |
+| [Group Security Dashboard](user/application_security/security_dashboard/index.md) **[ULTIMATE]** | View vulnerabilities in all the projects in a group and its subgroups. |
+| [License Management](user/application_security/license_management/index.md) **[ULTIMATE]** | Search your project's dependencies for their licenses. |
+| [Project Security Dashboard](user/application_security/security_dashboard/index.md) **[ULTIMATE]** | View the latest security reports for your project. |
+| [Static Application Security Testing (SAST)](user/application_security/sast/index.md) **[ULTIMATE]** | Analyze source code for known vulnerabilities. |
## Subscribe to GitLab
@@ -342,6 +369,8 @@ There are two ways to use GitLab:
- [GitLab.com](#gitlabcom): GitLab's SaaS offering. You don't need to install anything to use GitLab.com,
you only need to [sign up](https://gitlab.com/users/sign_in) and start using GitLab straight away.
+For more information on managing your subscription and [Customers Portal](https://customers.gitlab.com) account, please see [Getting Started with Subscriptions](getting-started/subscription.md).
+
The following sections outline tiers and features within GitLab self-managed and GitLab.com.
<div align="right">
@@ -393,6 +422,12 @@ GitLab.com subscriptions grant access
to the same features available in GitLab self-managed, **except
[administration](administration/index.md) tools and settings**.
+GitLab.com allows you to apply your subscription to a group or your personal user.
+
+When applied to a **group**, the group, all subgroups, and all projects under the selected group on GitLab.com will have the features of the associated plan. It is recommended to go with a group plan when managing projects and users of an organization.
+
+When associated with a **personal userspace** instead, all projects will have features with the subscription applied, but as it is not a group, group features will not be available.
+
TIP: **Tip:**
To support the open source community and encourage the development of open source projects, GitLab grants access to **Gold** features for all GitLab.com **public** projects, regardless of the subscription.
diff --git a/doc/administration/dependency_proxy.md b/doc/administration/dependency_proxy.md
new file mode 100644
index 00000000000..4dc1f4dcba4
--- /dev/null
+++ b/doc/administration/dependency_proxy.md
@@ -0,0 +1,150 @@
+# GitLab Dependency Proxy administration **[PREMIUM ONLY]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing) 11.11.
+
+GitLab can be utilized as a dependency proxy for a variety of common package managers.
+
+This is the administration documentation. If you want to learn how to use the
+dependency proxies, see the [user guide](../user/group/dependency_proxy/index.md).
+
+## Enabling the Dependency Proxy feature
+
+NOTE: **Note:**
+Dependency proxy requires the Puma web server to be enabled.
+Puma support is EXPERIMENTAL at this time.
+
+To enable the Dependency proxy feature:
+
+**Omnibus GitLab installations**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['dependency_proxy_enabled'] = true
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Enable the [Puma web server](https://docs.gitlab.com/omnibus/settings/puma.html).
+
+**Installations from source**
+
+1. After the installation is complete, you will have to configure the `dependency_proxy`
+ section in `config/gitlab.yml`. Set to `true` to enable it:
+
+ ```yaml
+ dependency_proxy:
+ enabled: true
+ ```
+
+1. [Restart GitLab] for the changes to take effect.
+1. Enable the [Puma web server](../install/installation.md#using-puma).
+
+## Changing the storage path
+
+By default, the dependency proxy files are stored locally, but you can change the default
+local location or even use object storage.
+
+### Changing the local storage path
+
+The dependency proxy files for Omnibus GitLab installations are stored under
+`/var/opt/gitlab/gitlab-rails/shared/dependency_proxy/` and for source
+installations under `shared/dependency_proxy/` (relative to the git home directory).
+To change the local storage path:
+
+**Omnibus GitLab installations**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['dependency_proxy_storage_path'] = "/mnt/dependency_proxy"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+**Installations from source**
+
+1. Edit the `dependency_proxy` section in `config/gitlab.yml`:
+
+ ```yaml
+ dependency_proxy:
+ enabled: true
+ storage_path: shared/dependency_proxy
+ ```
+1. [Restart GitLab] for the changes to take effect.
+
+### Using object storage
+
+Instead of relying on the local storage, you can use an object storage to
+upload the blobs of the dependency proxy:
+
+**Omnibus GitLab installations**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following lines (uncomment where
+ necessary):
+
+ ```ruby
+ gitlab_rails['dependency_proxy_enabled'] = true
+ gitlab_rails['dependency_proxy_storage_path'] = "/var/opt/gitlab/gitlab-rails/shared/dependency_proxy"
+ gitlab_rails['dependency_proxy_object_store_enabled'] = true
+ gitlab_rails['dependency_proxy_object_store_remote_directory'] = "dependency_proxy" # The bucket name.
+ gitlab_rails['dependency_proxy_object_store_direct_upload'] = false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false).
+ gitlab_rails['dependency_proxy_object_store_background_upload'] = true # Temporary option to limit automatic upload (Default: true).
+ gitlab_rails['dependency_proxy_object_store_proxy_download'] = false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage.
+ gitlab_rails['dependency_proxy_object_store_connection'] = {
+ ##
+ ## If the provider is AWS S3, uncomment the following
+ ##
+ #'provider' => 'AWS',
+ #'region' => 'eu-west-1',
+ #'aws_access_key_id' => 'AWS_ACCESS_KEY_ID',
+ #'aws_secret_access_key' => 'AWS_SECRET_ACCESS_KEY',
+ ##
+ ## If the provider is other than AWS (an S3-compatible one), uncomment the following
+ ##
+ #'host' => 's3.amazonaws.com',
+ #'aws_signature_version' => 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
+ #'endpoint' => 'https://s3.amazonaws.com' # Useful for S3-compliant services such as DigitalOcean Spaces.
+ #'path_style' => false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'.
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+**Installations from source**
+
+1. Edit the `dependency_proxy` section in `config/gitlab.yml` (uncomment where necessary):
+
+ ```yaml
+ dependency_proxy:
+ enabled: true
+ ##
+ ## The location where build dependency_proxy are stored (default: shared/dependency_proxy).
+ ##
+ #storage_path: shared/dependency_proxy
+ object_store:
+ enabled: false
+ remote_directory: dependency_proxy # The bucket name.
+ #direct_upload: false # Use Object Storage directly for uploads instead of background uploads if enabled (Default: false).
+ #background_upload: true # Temporary option to limit automatic upload (Default: true).
+ #proxy_download: false # Passthrough all downloads via GitLab instead of using Redirects to Object Storage.
+ connection:
+ ##
+ ## If the provider is AWS S3, uncomment the following
+ ##
+ #provider: AWS
+ #region: us-east-1
+ #aws_access_key_id: AWS_ACCESS_KEY_ID
+ #aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ ##
+ ## If the provider is other than AWS (an S3-compatible one), uncomment the following
+ ##
+ #host: 's3.amazonaws.com' # default: s3.amazonaws.com.
+ #aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
+ #endpoint: 'https://s3.amazonaws.com' # Useful for S3-compliant services such as DigitalOcean Spaces.
+ #path_style: false # If true, use 'host/bucket_name/object' instead of 'bucket_name.host/object'.
+ ```
+
+1. [Restart GitLab] for the changes to take effect.
+
+[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
+[restart gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
diff --git a/doc/administration/geo/disaster_recovery/background_verification.md b/doc/administration/geo/disaster_recovery/background_verification.md
index 7d2fd51f834..c7299b6e196 100644
--- a/doc/administration/geo/disaster_recovery/background_verification.md
+++ b/doc/administration/geo/disaster_recovery/background_verification.md
@@ -157,6 +157,42 @@ For wikis:
sudo -u git -H bundle exec rake geo:verification:wiki:reset RAILS_ENV=production
```
+## Reconcile differences with checksum mismatches
+
+If the **primary** and **secondary** nodes have a checksum verification mismatch, the cause may not be apparent. To find the cause of a checksum mismatch:
+
+1. Navigate to the **Admin Area > Projects** dashboard on the **primary** node, find the
+ project that you want to check the checksum differences and click on the
+ **Edit** button:
+ ![Projects dashboard](img/checksum-differences-admin-projects.png)
+
+1. On the project admin page get the **Gitaly storage name**, and **Gitaly relative path**:
+ ![Project admin page](img/checksum-differences-admin-project-page.png)
+
+1. Navigate to the project's repository directory on both **primary** and **secondary** nodes. For an installation from source, the path is usually `/home/git/repositories`. For Omnibus installs, the path is usually `/var/opt/gitlab/git-data/repositories`. Note that if `git_data_dirs` is customized, check the directory layout on your server to be sure.
+
+ ```sh
+ cd /var/opt/gitlab/git-data/repositories
+ ```
+
+1. Run the following command on the **primary** node, redirecting the output to a file:
+
+ ```sh
+ git show-ref --head | grep -E "HEAD|(refs/(heads|tags|keep-around|merge-requests|environments|notes)/)" > primary-node-refs
+ ```
+
+1. Run the following command on the **secondary** node, redirecting the output to a file:
+
+ ```sh
+ git show-ref --head | grep -E "HEAD|(refs/(heads|tags|keep-around|merge-requests|environments|notes)/)" > secondary-node-refs
+ ```
+
+1. Copy the files from the previous steps on the same system, and do a diff between the contents:
+
+ ```sh
+ diff primary-node-refs secondary-node-refs
+ ```
+
## Current limitations
Until [issue #5064][ee-5064] is completed, background verification doesn't cover
diff --git a/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.png b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.png
new file mode 100644
index 00000000000..fd51523104b
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.png
Binary files differ
diff --git a/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.png b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.png
new file mode 100644
index 00000000000..b2a6da69d3d
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.png
Binary files differ
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index d0e0e320019..caadec3ac4e 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -41,7 +41,6 @@ options:
NOTE: **Note:**
This is only available starting in certain versions of GitLab:
-
* 11.5.11
* 11.6.11
* 11.7.12
@@ -59,13 +58,13 @@ details.
To do this, run the Rake task:
```sh
-gitlab-rake gitlab:features:enable_rugged
+sudo gitlab-rake gitlab:features:enable_rugged
```
If you need to undo this setting for some reason, run:
```sh
-gitlab-rake gitlab:features:disable_rugged
+sudo gitlab-rake gitlab:features:disable_rugged
```
### Known issues
diff --git a/doc/administration/high_availability/nfs_host_client_setup.md b/doc/administration/high_availability/nfs_host_client_setup.md
index a8bc101dee6..a8d69b9ab0a 100644
--- a/doc/administration/high_availability/nfs_host_client_setup.md
+++ b/doc/administration/high_availability/nfs_host_client_setup.md
@@ -67,7 +67,7 @@ apt-get install nfs-common
### Step 2 - Create Mount Points on Client
-Create a directroy on the client that we can mount the shared directory from the host.
+Create a directory on the client that we can mount the shared directory from the host.
Please note that if your mount point directory contains any files they will be hidden
once the remote shares are mounted. An empty/new directory on the client is recommended
for this purpose.
diff --git a/doc/administration/img/high_availability/fully-distributed.png b/doc/administration/img/high_availability/fully-distributed.png
deleted file mode 100644
index ad23207134e..00000000000
--- a/doc/administration/img/high_availability/fully-distributed.png
+++ /dev/null
Binary files differ
diff --git a/doc/administration/img/high_availability/geo-ha-diagram.png b/doc/administration/img/high_availability/geo-ha-diagram.png
deleted file mode 100644
index da5d612827c..00000000000
--- a/doc/administration/img/high_availability/geo-ha-diagram.png
+++ /dev/null
Binary files differ
diff --git a/doc/administration/img/high_availability/horizontal.png b/doc/administration/img/high_availability/horizontal.png
deleted file mode 100644
index c3bd489d96f..00000000000
--- a/doc/administration/img/high_availability/horizontal.png
+++ /dev/null
Binary files differ
diff --git a/doc/administration/img/high_availability/hybrid.png b/doc/administration/img/high_availability/hybrid.png
deleted file mode 100644
index 7d4a56bf0ea..00000000000
--- a/doc/administration/img/high_availability/hybrid.png
+++ /dev/null
Binary files differ
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 48bd709a2b7..c243dd9edbb 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -43,11 +43,10 @@ The following metrics are available:
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
| upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file |
-| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
-| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
-| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) |
-| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections |
-| unicorn_workers | Gauge | 11.11 | The number of Unicorn workers |
+| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
+| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
+| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) |
+| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections |
## Sidekiq Metrics available for Geo **[PREMIUM]**
@@ -101,10 +100,6 @@ Some basic Ruby runtime metrics are available:
| ruby_file_descriptors | Gauge | 11.1 | File descriptors per process |
| ruby_memory_bytes | Gauge | 11.1 | Memory usage by process |
| ruby_sampler_duration_seconds_total | Counter | 11.1 | Time spent collecting stats |
-| ruby_process_cpu_seconds_total | Gauge | 11.11 | Total amount of CPU time per process |
-| ruby_process_max_fds | Gauge | 11.11 | Maximum number of open file descriptors per process |
-| ruby_process_resident_memory_bytes | Gauge | 11.11 | Memory usage by process, measured in bytes |
-| ruby_process_start_time_seconds | Gauge | 11.11 | The elapsed time between system boot and the process started, measured in seconds |
[GC.stat]: https://ruby-doc.org/core-2.3.0/GC.html#method-c-stat
diff --git a/doc/api/epic_issues.md b/doc/api/epic_issues.md
new file mode 100644
index 00000000000..438a3361dcc
--- /dev/null
+++ b/doc/api/epic_issues.md
@@ -0,0 +1,412 @@
+# Epic Issues API **[ULTIMATE]**
+
+Every API call to epic_issues must be authenticated.
+
+If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code.
+
+Epics are available only in Ultimate. If epics feature is not available a `403` status code will be returned.
+
+## List issues for an epic
+Gets all issues that are assigned to an epic and the authenticated user has access to.
+
+```
+GET /groups/:id/epics/:epic_iid/issues
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid` | integer/string | yes | The internal ID of the epic. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 76,
+ "iid": 6,
+ "project_id": 8,
+ "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
+ "description" : "Ratione dolores corrupti mollitia soluta quia.",
+ "state": "opened",
+ "created_at": "2017-11-15T13:39:24.670Z",
+ "updated_at": "2018-01-04T10:49:19.506Z",
+ "closed_at": null,
+ "labels": [],
+ "milestone": {
+ "id": 38,
+ "iid": 3,
+ "project_id": 8,
+ "title": "v2.0",
+ "description": "In tempore culpa inventore quo accusantium.",
+ "state": "closed",
+ "created_at": "2017-11-15T13:39:13.825Z",
+ "updated_at": "2017-11-15T13:39:13.825Z",
+ "due_date": null,
+ "start_date": null
+ },
+ "assignees": [{
+ "id": 7,
+ "name": "Pamella Huel",
+ "username": "arnita",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon",
+ "web_url": "http://localhost:3001/arnita"
+ }],
+ "assignee": {
+ "id": 7,
+ "name": "Pamella Huel",
+ "username": "arnita",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon",
+ "web_url": "http://localhost:3001/arnita"
+ },
+ "author": {
+ "id": 13,
+ "name": "Michell Johns",
+ "username": "chris_hahn",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/30e3b2122ccd6b8e45e8e14a3ffb58fc?s=80&d=identicon",
+ "web_url": "http://localhost:3001/chris_hahn"
+ },
+ "user_notes_count": 8,
+ "upvotes": 0,
+ "downvotes": 0,
+ "due_date": null,
+ "confidential": false,
+ "weight": null,
+ "discussion_locked": null,
+ "web_url": "http://localhost:3001/h5bp/html5-boilerplate/issues/6",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
+ "_links":{
+ "self": "http://localhost:3001/api/v4/projects/8/issues/6",
+ "notes": "http://localhost:3001/api/v4/projects/8/issues/6/notes",
+ "award_emoji": "http://localhost:3001/api/v4/projects/8/issues/6/award_emoji",
+ "project": "http://localhost:3001/api/v4/projects/8"
+ },
+ "subscribed": true,
+ "epic_issue_id": 2
+ }
+]
+```
+
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+
+## Assign an issue to the epic
+
+Creates an epic - issue association. If the issue in question belongs to another epic it is unassigned from that epic.
+
+```
+POST /groups/:id/epics/:epic_iid/issues/:issue_id
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid` | integer/string | yes | The internal ID of the epic. |
+| `issue_id` | integer/string | yes | The ID of the issue. |
+
+```bash
+curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/55
+```
+
+Example response:
+
+```json
+{
+ "id": 11,
+ "epic": {
+ "id": 30,
+ "iid": 5,
+ "title": "Ea cupiditate dolores ut vero consequatur quasi veniam voluptatem et non.",
+ "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
+ "author": {
+ "id": 7,
+ "name": "Pamella Huel",
+ "username": "arnita",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon",
+ "web_url": "http://localhost:3001/arnita"
+ },
+ "start_date": null,
+ "end_date": null
+ },
+ "issue": {
+ "id": 55,
+ "iid": 13,
+ "project_id": 8,
+ "title": "Beatae laborum voluptatem voluptate eligendi ex accusamus.",
+ "description": "Quam veritatis debitis omnis aliquam sit.",
+ "state": "opened",
+ "created_at": "2017-11-05T13:59:12.782Z",
+ "updated_at": "2018-01-05T10:33:03.900Z",
+ "closed_at": null,
+ "labels": [],
+ "milestone": {
+ "id": 48,
+ "iid": 6,
+ "project_id": 8,
+ "title": "Sprint - Sed sed maxime temporibus ipsa ullam qui sit.",
+ "description": "Quos veritatis qui expedita sunt deleniti accusamus.",
+ "state": "active",
+ "created_at": "2017-11-05T13:59:12.445Z",
+ "updated_at": "2017-11-05T13:59:12.445Z",
+ "due_date": "2017-11-13",
+ "start_date": "2017-11-05"
+ },
+ "assignees": [{
+ "id": 10,
+ "name": "Lu Mayer",
+ "username": "kam",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
+ "web_url": "http://localhost:3001/kam"
+ }],
+ "assignee": {
+ "id": 10,
+ "name": "Lu Mayer",
+ "username": "kam",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
+ "web_url": "http://localhost:3001/kam"
+ },
+ "author": {
+ "id": 25,
+ "name": "User 3",
+ "username": "user3",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80&d=identicon",
+ "web_url": "http://localhost:3001/user3"
+ },
+ "user_notes_count": 0,
+ "upvotes": 0,
+ "downvotes": 0,
+ "due_date": null,
+ "confidential": false,
+ "weight": null,
+ "discussion_locked": null,
+ "web_url": "http://localhost:3001/h5bp/html5-boilerplate/issues/13",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
+ }
+}
+```
+
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+
+## Remove an issue from the epic
+
+Removes an epic - issue association.
+
+```
+DELETE /groups/:id/epics/:epic_iid/issues/:epic_issue_id
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | -----------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid` | integer/string | yes | The internal ID of the epic. |
+| `epic_issue_id` | integer/string | yes | The ID of the issue - epic association. |
+
+```bash
+curl --header DELETE "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/11
+```
+
+Example response:
+
+```json
+{
+ "id": 11,
+ "epic": {
+ "id": 30,
+ "iid": 5,
+ "title": "Ea cupiditate dolores ut vero consequatur quasi veniam voluptatem et non.",
+ "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
+ "author": {
+ "id": 7,
+ "name": "Pamella Huel",
+ "username": "arnita",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon",
+ "web_url": "http://localhost:3001/arnita"
+ },
+ "start_date": null,
+ "end_date": null
+ },
+ "issue": {
+ "id": 223,
+ "iid": 13,
+ "project_id": 8,
+ "title": "Beatae laborum voluptatem voluptate eligendi ex accusamus.",
+ "description": "Quam veritatis debitis omnis aliquam sit.",
+ "state": "opened",
+ "created_at": "2017-11-05T13:59:12.782Z",
+ "updated_at": "2018-01-05T10:33:03.900Z",
+ "closed_at": null,
+ "labels": [],
+ "milestone": {
+ "id": 48,
+ "iid": 6,
+ "project_id": 8,
+ "title": "Sprint - Sed sed maxime temporibus ipsa ullam qui sit.",
+ "description": "Quos veritatis qui expedita sunt deleniti accusamus.",
+ "state": "active",
+ "created_at": "2017-11-05T13:59:12.445Z",
+ "updated_at": "2017-11-05T13:59:12.445Z",
+ "due_date": "2017-11-13",
+ "start_date": "2017-11-05"
+ },
+ "assignees": [{
+ "id": 10,
+ "name": "Lu Mayer",
+ "username": "kam",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
+ "web_url": "http://localhost:3001/kam"
+ }],
+ "assignee": {
+ "id": 10,
+ "name": "Lu Mayer",
+ "username": "kam",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
+ "web_url": "http://localhost:3001/kam"
+ },
+ "author": {
+ "id": 25,
+ "name": "User 3",
+ "username": "user3",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80&d=identicon",
+ "web_url": "http://localhost:3001/user3"
+ },
+ "user_notes_count": 0,
+ "upvotes": 0,
+ "downvotes": 0,
+ "due_date": null,
+ "confidential": false,
+ "weight": null,
+ "discussion_locked": null,
+ "web_url": "http://localhost:3001/h5bp/html5-boilerplate/issues/13",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
+ }
+}
+```
+
+**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
+
+## Update epic - issue association
+
+Updates an epic - issue association.
+
+```
+PUT /groups/:id/epics/:epic_iid/issues/:epic_issue_id
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | -----------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid` | integer/string | yes | The internal ID of the epic. |
+| `epic_issue_id` | integer/string | yes | The ID of the issue - epic association. |
+| `move_before_id` | integer/string | no | The ID of the issue - epic association that should be placed before the link in the question. |
+| `move_after_id` | integer/string | no | The ID of the issue - epic association that should be placed after the link in the question. |
+
+```bash
+curl --header PUT "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/issues/11?move_before_id=20
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 30,
+ "iid": 6,
+ "project_id": 8,
+ "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
+ "description" : "Ratione dolores corrupti mollitia soluta quia.",
+ "state": "opened",
+ "created_at": "2017-11-15T13:39:24.670Z",
+ "updated_at": "2018-01-04T10:49:19.506Z",
+ "closed_at": null,
+ "labels": [],
+ "milestone": {
+ "id": 38,
+ "iid": 3,
+ "project_id": 8,
+ "title": "v2.0",
+ "description": "In tempore culpa inventore quo accusantium.",
+ "state": "closed",
+ "created_at": "2017-11-15T13:39:13.825Z",
+ "updated_at": "2017-11-15T13:39:13.825Z",
+ "due_date": null,
+ "start_date": null
+ },
+ "assignees": [{
+ "id": 7,
+ "name": "Pamella Huel",
+ "username": "arnita",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon",
+ "web_url": "http://localhost:3001/arnita"
+ }],
+ "assignee": {
+ "id": 7,
+ "name": "Pamella Huel",
+ "username": "arnita",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon",
+ "web_url": "http://localhost:3001/arnita"
+ },
+ "author": {
+ "id": 13,
+ "name": "Michell Johns",
+ "username": "chris_hahn",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/30e3b2122ccd6b8e45e8e14a3ffb58fc?s=80&d=identicon",
+ "web_url": "http://localhost:3001/chris_hahn"
+ },
+ "user_notes_count": 8,
+ "upvotes": 0,
+ "downvotes": 0,
+ "due_date": null,
+ "confidential": false,
+ "weight": null,
+ "discussion_locked": null,
+ "web_url": "http://localhost:3001/h5bp/html5-boilerplate/issues/6",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
+ "_links":{
+ "self": "http://localhost:3001/api/v4/projects/8/issues/6",
+ "notes": "http://localhost:3001/api/v4/projects/8/issues/6/notes",
+ "award_emoji": "http://localhost:3001/api/v4/projects/8/issues/6/award_emoji",
+ "project": "http://localhost:3001/api/v4/projects/8"
+ },
+ "subscribed": true,
+ "epic_issue_id": 11,
+ "relative_position": 55
+ }
+]
+```
diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md
new file mode 100644
index 00000000000..619ae6ea2dc
--- /dev/null
+++ b/doc/api/epic_links.md
@@ -0,0 +1,252 @@
+# Epic Links API **[ULTIMATE]**
+
+>**Note:**
+> This endpoint was [introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9188) in GitLab 11.8.
+
+Manages parent-child [epic relationships](../user/group/epics/index.md#multi-level-child-epics).
+
+Every API call to `epic_links` must be authenticated.
+
+If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code.
+
+Epics are available only in the [Ultimate/Gold tier](https://about.gitlab.com/pricing/). If the epics feature is not available, a `403` status code will be returned.
+
+## List epics related to a given epic
+Gets all child epics of an epic.
+
+```
+GET /groups/:id/epics/:epic_iid/epics
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid` | integer | yes | The internal ID of the epic. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/epics/
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 29,
+ "iid": 6,
+ "group_id": 1,
+ "parent_id": 5,
+ "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.",
+ "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
+ "author": {
+ "id": 10,
+ "name": "Lu Mayer",
+ "username": "kam",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
+ "web_url": "http://localhost:3001/kam"
+ },
+ "start_date": null,
+ "start_date_is_fixed": false,
+ "start_date_fixed": null,
+ "start_date_from_milestones": null,
+ "end_date": "2018-07-31",
+ "due_date": "2018-07-31",
+ "due_date_is_fixed": false,
+ "due_date_fixed": null,
+ "due_date_from_milestones": "2018-07-31",
+ "created_at": "2018-07-17T13:36:22.770Z",
+ "updated_at": "2018-07-18T12:22:05.239Z",
+ "labels": []
+ }
+]
+```
+
+## Assign a child epic
+
+Creates an association between two epics, designating one as the parent epic and the other as the child epic. A parent epic can have multiple child epics. If the new child epic already belonged to another epic, it is unassigned from that previous parent.
+
+```
+POST /groups/:id/epics/:epic_iid/epics
+```
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid` | integer | yes | The internal ID of the epic. |
+| `child_epic_id` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. |
+
+```bash
+curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/epics/6
+```
+
+Example response:
+
+```json
+{
+ "id": 6,
+ "iid": 38,
+ "group_id": 1,
+ "parent_id": 5
+ "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.",
+ "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
+ "author": {
+ "id": 10,
+ "name": "Lu Mayer",
+ "username": "kam",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
+ "web_url": "http://localhost:3001/kam"
+ },
+ "start_date": null,
+ "start_date_is_fixed": false,
+ "start_date_fixed": null,
+ "start_date_from_milestones": null,
+ "end_date": "2018-07-31",
+ "due_date": "2018-07-31",
+ "due_date_is_fixed": false,
+ "due_date_fixed": null,
+ "due_date_from_milestones": "2018-07-31",
+ "created_at": "2018-07-17T13:36:22.770Z",
+ "updated_at": "2018-07-18T12:22:05.239Z",
+ "labels": []
+}
+```
+
+## Create and assign a child epic
+
+Creates a a new epic and associates it with provided parent epic. The response is LinkedEpic object.
+
+```
+POST /groups/:id/epics/:epic_iid/epics
+```
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid` | integer | yes | The internal ID of the (future parent) epic. |
+| `title` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. |
+
+```bash
+curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/epics?title=Newpic
+```
+
+Example response:
+
+```json
+{
+ "id": 24,
+ "iid": 2,
+ "title": "child epic",
+ "group_id": 49,
+ "parent_id": 23,
+ "has_children": false,
+ "reference": "&2",
+ "url": "http://localhost/groups/group16/-/epics/2",
+ "relation_url": "http://localhost/groups/group16/-/epics/1/links/24"
+}
+```
+
+## Re-order a child epic
+
+```
+PUT /groups/:id/epics/:epic_iid/epics/:child_epic_id
+```
+
+| Attribute | Type | Required | Description |
+| ---------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `epic_iid` | integer | yes | The internal ID of the epic. |
+| `child_epic_id` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. |
+| `move_before_id` | integer | no | The global ID of a sibling epic that should be placed before the child epic. |
+| `move_after_id` | integer | no | The global ID of a sibling epic that should be placed after the child epic. |
+
+```bash
+curl --header PUT "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/4/epics/5
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 29,
+ "iid": 6,
+ "group_id": 1,
+ "parent_id": 5,
+ "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.",
+ "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
+ "author": {
+ "id": 10,
+ "name": "Lu Mayer",
+ "username": "kam",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
+ "web_url": "http://localhost:3001/kam"
+ },
+ "start_date": null,
+ "start_date_is_fixed": false,
+ "start_date_fixed": null,
+ "start_date_from_milestones": null,
+ "end_date": "2018-07-31",
+ "due_date": "2018-07-31",
+ "due_date_is_fixed": false,
+ "due_date_fixed": null,
+ "due_date_from_milestones": "2018-07-31",
+ "created_at": "2018-07-17T13:36:22.770Z",
+ "updated_at": "2018-07-18T12:22:05.239Z",
+ "labels": []
+ }
+]
+```
+
+## Unassign a child epic
+
+Unassigns a child epic from a parent epic.
+
+```
+DELETE /groups/:id/epics/:epic_iid/epics/:child_epic_id
+```
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `epic_iid` | integer | yes | The internal ID of the epic. |
+| `child_epic_id` | integer | yes | The global ID of the child epic. Internal ID can't be used because they can conflict with epics from other groups. |
+
+```bash
+curl --header DELETE "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/4/epics/5
+```
+
+Example response:
+
+```json
+{
+ "id": 5,
+ "iid": 38,
+ "group_id": 1,
+ "parent_id": null,
+ "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.",
+ "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
+ "author": {
+ "id": 10,
+ "name": "Lu Mayer",
+ "username": "kam",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
+ "web_url": "http://localhost:3001/kam"
+ },
+ "start_date": null,
+ "start_date_is_fixed": false,
+ "start_date_fixed": null,
+ "start_date_from_milestones": null,
+ "end_date": "2018-07-31",
+ "due_date": "2018-07-31",
+ "due_date_is_fixed": false,
+ "due_date_fixed": null,
+ "due_date_from_milestones": "2018-07-31",
+ "created_at": "2018-07-17T13:36:22.770Z",
+ "updated_at": "2018-07-18T12:22:05.239Z",
+ "labels": []
+}
+```
diff --git a/doc/api/epics.md b/doc/api/epics.md
new file mode 100644
index 00000000000..0541cfaa715
--- /dev/null
+++ b/doc/api/epics.md
@@ -0,0 +1,360 @@
+# Epics API **[ULTIMATE]**
+
+Every API call to epic must be authenticated.
+
+If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code.
+
+If epics feature is not available a `403` status code will be returned.
+
+## Epic issues API
+
+The [epic issues API](epic_issues.md) allows you to interact with issues associated with an epic.
+
+# Milestone dates integration
+
+> [Introduced][ee-6448] in GitLab 11.3.
+
+Since start date and due date can be dynamically sourced from related issue milestones, when user has edit permission, additional fields will be shown. These include two boolean fields `start_date_is_fixed` and `due_date_is_fixed`, and four date fields `start_date_fixed`, `start_date_from_milestones`, `due_date_fixed` and `due_date_from_milestones`.
+
+`end_date` has been deprecated in favor of `due_date`.
+
+## Epics pagination
+
+By default, `GET` requests return 20 results at a time because the API results
+are paginated.
+
+Read more on [pagination](README.md#pagination).
+
+## List epics for a group
+
+Gets all epics of the requested group and its subgroups.
+
+```
+GET /groups/:id/epics
+GET /groups/:id/epics?author_id=5
+GET /groups/:id/epics?labels=bug,reproduced
+GET /groups/:id/epics?state=opened
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `author_id` | integer | no | Return epics created by the given user `id` |
+| `labels` | string | no | Return epics matching a comma separated list of labels names. Label names from the epic group or a parent group can be used |
+| `order_by` | string | no | Return epics ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return epics sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Search epics against their `title` and `description` |
+| `state` | string | no | Search epics against their `state`, possible filters: `opened`, `closed` and `all`, default: `all` |
+| `created_after` | datetime | no | Return epics created on or after the given time |
+| `created_before` | datetime | no | Return epics created on or before the given time |
+| `updated_after` | datetime | no | Return epics updated on or after the given time |
+| `updated_before` | datetime | no | Return epics updated on or before the given time |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 29,
+ "iid": 4,
+ "group_id": 7,
+ "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.",
+ "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
+ "state": "opened",
+ "author": {
+ "id": 10,
+ "name": "Lu Mayer",
+ "username": "kam",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/018729e129a6f31c80a6327a30196823?s=80&d=identicon",
+ "web_url": "http://localhost:3001/kam"
+ },
+ "start_date": null,
+ "start_date_is_fixed": false,
+ "start_date_fixed": null,
+ "start_date_from_milestones": null,
+ "end_date": "2018-07-31",
+ "due_date": "2018-07-31",
+ "due_date_is_fixed": false,
+ "due_date_fixed": null,
+ "due_date_from_milestones": "2018-07-31",
+ "created_at": "2018-07-17T13:36:22.770Z",
+ "updated_at": "2018-07-18T12:22:05.239Z",
+ "labels": [],
+ "upvotes": 4,
+ "downvotes": 0
+ }
+]
+```
+
+## Single epic
+
+Gets a single epic
+
+```
+GET /groups/:id/epics/:epic_iid
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid` | integer/string | yes | The internal ID of the epic. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5
+```
+
+Example response:
+
+```json
+{
+ "id": 30,
+ "iid": 5,
+ "group_id": 7,
+ "title": "Ea cupiditate dolores ut vero consequatur quasi veniam voluptatem et non.",
+ "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
+ "state": "opened",
+ "author":{
+ "id": 7,
+ "name": "Pamella Huel",
+ "username": "arnita",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon",
+ "web_url": "http://localhost:3001/arnita"
+ },
+ "start_date": null,
+ "start_date_is_fixed": false,
+ "start_date_fixed": null,
+ "start_date_from_milestones": null,
+ "end_date": "2018-07-31",
+ "due_date": "2018-07-31",
+ "due_date_is_fixed": false,
+ "due_date_fixed": null,
+ "due_date_from_milestones": "2018-07-31",
+ "created_at": "2018-07-17T13:36:22.770Z",
+ "updated_at": "2018-07-18T12:22:05.239Z",
+ "labels": [],
+ "upvotes": 4,
+ "downvotes": 0
+}
+```
+
+## New epic
+
+Creates a new epic.
+
+NOTE: **Note:**
+Starting with GitLab [11.3][ee-6448], `start_date` and `end_date` should no longer be assigned
+directly, as they now represent composite values. You can configure it via the `*_is_fixed` and
+`*_fixed` fields instead.
+
+```
+POST /groups/:id/epics
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `title` | string | yes | The title of the epic |
+| `labels` | string | no | The comma separated list of labels |
+| `description` | string | no | The description of the epic |
+| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
+| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
+| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
+| `due_date_fixed` | string | no | The fixed due date of an epic (since 11.3) |
+| `parent_id` | integer/string | no | The id of a parent epic (since 11.11) |
+
+```bash
+curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics?title=Epic&description=Epic%20description
+```
+
+Example response:
+
+```json
+{
+ "id": 33,
+ "iid": 6,
+ "group_id": 7,
+ "title": "Epic",
+ "description": "Epic description",
+ "state": "opened",
+ "author": {
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/eileen.lowe",
+ "id" : 18,
+ "username" : "eileen.lowe"
+ },
+ "start_date": null,
+ "start_date_is_fixed": false,
+ "start_date_fixed": null,
+ "start_date_from_milestones": null,
+ "end_date": "2018-07-31",
+ "due_date": "2018-07-31",
+ "due_date_is_fixed": false,
+ "due_date_fixed": null,
+ "due_date_from_milestones": "2018-07-31",
+ "created_at": "2018-07-17T13:36:22.770Z",
+ "updated_at": "2018-07-18T12:22:05.239Z",
+ "labels": [],
+ "upvotes": 4,
+ "downvotes": 0
+}
+```
+
+## Update epic
+
+Updates an epic.
+
+NOTE: **Note:**
+Starting with GitLab [11.3][ee-6448], `start_date` and `end_date` should no longer be assigned
+directly, as they now represent composite values. You can configure it via the `*_is_fixed` and
+`*_fixed` fields instead.
+
+```
+PUT /groups/:id/epics/:epic_iid
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid` | integer/string | yes | The internal ID of the epic |
+| `title` | string | no | The title of an epic |
+| `description` | string | no | The description of an epic |
+| `labels` | string | no | The comma separated list of labels |
+| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
+| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
+| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
+| `due_date_fixed` | string | no | The fixed due date of an epic (since 11.3) |
+| `state_event` | string | no | State event for an epic. Set `close` to close the epic and `reopen` to reopen it (since 11.4) |
+
+```bash
+curl --header PUT "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5?title=New%20Title
+```
+
+Example response:
+
+```json
+{
+ "id": 33,
+ "iid": 6,
+ "group_id": 7,
+ "title": "New Title",
+ "description": "Epic description",
+ "state": "opened",
+ "author": {
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/eileen.lowe",
+ "id" : 18,
+ "username" : "eileen.lowe"
+ },
+ "start_date": null,
+ "start_date_is_fixed": false,
+ "start_date_fixed": null,
+ "start_date_from_milestones": null,
+ "end_date": "2018-07-31",
+ "due_date": "2018-07-31",
+ "due_date_is_fixed": false,
+ "due_date_fixed": null,
+ "due_date_from_milestones": "2018-07-31",
+ "created_at": "2018-07-17T13:36:22.770Z",
+ "updated_at": "2018-07-18T12:22:05.239Z",
+ "labels": [],
+ "upvotes": 4,
+ "downvotes": 0
+}
+```
+
+## Delete epic
+
+Deletes an epic
+
+```
+DELETE /groups/:id/epics/:epic_iid
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid` | integer/string | yes | The internal ID of the epic. |
+
+```bash
+curl --header DELETE "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5
+```
+
+## Create a todo
+
+Manually creates a todo for the current user on an epic. If
+there already exists a todo for the user on that epic, status code `304` is
+returned.
+
+```
+POST /groups/:id/epics/:epic_iid/todo
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|--------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `epic_iid ` | integer | yes | The internal ID of a group's epic |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/5/todo
+```
+
+Example response:
+
+```json
+{
+ "id": 112,
+ "group": {
+ "id": 1,
+ "name": "Gitlab",
+ "path": "gitlab",
+ "kind": "group",
+ "full_path": "base/gitlab",
+ "parent_id": null
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/root"
+ },
+ "action_name": "marked",
+ "target_type": "epic",
+ "target": {
+ "id": 30,
+ "iid": 5,
+ "group_id": 1,
+ "title": "Ea cupiditate dolores ut vero consequatur quasi veniam voluptatem et non.",
+ "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
+ "author":{
+ "id": 7,
+ "name": "Pamella Huel",
+ "username": "arnita",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a2f5c6fcef64c9c69cb8779cb292be1b?s=80&d=identicon",
+ "web_url": "http://localhost:3001/arnita"
+ },
+ "start_date": null,
+ "end_date": null,
+ "created_at": "2018-01-21T06:21:13.165Z",
+ "updated_at": "2018-01-22T12:41:41.166Z"
+ },
+ "target_url": "https://gitlab.example.com/groups/epics/5",
+ "body": "Vel voluptas atque dicta mollitia adipisci qui at.",
+ "state": "pending",
+ "created_at": "2016-07-01T11:09:13.992Z"
+}
+```
+
+[ee-6448]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6448
diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md
new file mode 100644
index 00000000000..a1cb524499f
--- /dev/null
+++ b/doc/api/geo_nodes.md
@@ -0,0 +1,408 @@
+# Geo Nodes API **[PREMIUM ONLY]**
+
+In order to interact with Geo node endpoints, you need to authenticate yourself
+as an admin.
+
+## Retrieve configuration about all Geo nodes
+
+```
+GET /geo_nodes
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "name": "us-node",
+ "url": "https://primary.example.com/",
+ "internal_url": "https://internal.example.com/",
+ "primary": true,
+ "enabled": true,
+ "current": true,
+ "files_max_capacity": 10,
+ "repos_max_capacity": 25,
+ "verification_max_capacity": 100,
+ "clone_protocol": "http"
+ },
+ {
+ "id": 2,
+ "name": "cn-node",
+ "url": "https://secondary.example.com/",
+ "internal_url": "https://secondary.example.com/",
+ "primary": false,
+ "enabled": true,
+ "current": false,
+ "files_max_capacity": 10,
+ "repos_max_capacity": 25,
+ "verification_max_capacity": 100,
+ "clone_protocol": "http"
+ }
+]
+```
+
+## Retrieve configuration about a specific Geo node
+
+```
+GET /geo_nodes/:id
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/1
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "name": "us-node",
+ "url": "https://primary.example.com/",
+ "internal_url": "https://primary.example.com/",
+ "primary": true,
+ "enabled": true,
+ "current": true,
+ "files_max_capacity": 10,
+ "repos_max_capacity": 25,
+ "verification_max_capacity": 100,
+ "clone_protocol": "http"
+}
+```
+
+## Edit a Geo node
+
+Updates settings of an existing Geo node.
+
+_This can only be run against a primary Geo node._
+
+```
+PUT /geo_nodes/:id
+```
+
+| Attribute | Type | Required | Description |
+|----------------------|---------|-----------|---------------------------------------------------------------------------|
+| `id` | integer | yes | The ID of the Geo node. |
+| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. |
+| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. |
+| `url` | string | yes | The user-facing URL of the Geo node. |
+| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.|
+| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. |
+| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. |
+| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. |
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "name": "cn-node",
+ "url": "https://secondary.example.com/",
+ "internal_url": "https://secondary.example.com/",
+ "primary": false,
+ "enabled": true,
+ "current": true,
+ "files_max_capacity": 10,
+ "repos_max_capacity": 25,
+ "verification_max_capacity": 100,
+ "clone_protocol": "http"
+}
+```
+
+## Delete a Geo node
+
+Removes the Geo node.
+
+NOTE: **Note:**
+Only a Geo primary node will accept this request.
+
+```
+DELETE /geo_nodes/:id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------|
+| `id` | integer | yes | The ID of the Geo node. |
+
+## Repair a Geo node
+
+To repair the OAuth authentication of a Geo node.
+
+_This can only be run against a primary Geo node._
+
+```
+POST /geo_nodes/:id/repair
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "name": "us-node",
+ "url": "https://primary.example.com/",
+ "internal_url": "https://primary.example.com/",
+ "primary": true,
+ "enabled": true,
+ "current": true,
+ "files_max_capacity": 10,
+ "repos_max_capacity": 25,
+ "verification_max_capacity": 100,
+ "clone_protocol": "http"
+}
+```
+
+## Retrieve status about all Geo nodes
+
+```
+GET /geo_nodes/status
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/status
+```
+
+Example response:
+
+```json
+[
+ {
+ "geo_node_id": 1,
+ "healthy": true,
+ "health": "Healthy",
+ "health_status": "Healthy",
+ "missing_oauth_application": false,
+ "attachments_count": 1,
+ "attachments_synced_count": nil,
+ "attachments_failed_count": nil,
+ "attachments_synced_missing_on_primary_count": 0,
+ "attachments_synced_in_percentage": "0.00%",
+ "db_replication_lag_seconds": nil,
+ "lfs_objects_count": 0,
+ "lfs_objects_synced_count": nil,
+ "lfs_objects_failed_count": nil,
+ "lfs_objects_synced_missing_on_primary_count": 0,
+ "lfs_objects_synced_in_percentage": "0.00%",
+ "job_artifacts_count": 2,
+ "job_artifacts_synced_count": nil,
+ "job_artifacts_failed_count": nil,
+ "job_artifacts_synced_missing_on_primary_count": 0,
+ "job_artifacts_synced_in_percentage": "0.00%",
+ "repositories_count": 41,
+ "projects_count": 41,
+ "repositories_failed_count": nil,
+ "repositories_synced_count": nil,
+ "repositories_synced_in_percentage": "0.00%",
+ "wikis_count": 41,
+ "wikis_failed_count": nil,
+ "wikis_synced_count": nil,
+ "wikis_synced_in_percentage": "0.00%",
+ "replication_slots_count": 1,
+ "replication_slots_used_count": 1,
+ "replication_slots_used_in_percentage": "100.00%",
+ "replication_slots_max_retained_wal_bytes": 0,
+ "repositories_checked_count": 20,
+ "repositories_checked_failed_count": 20,
+ "repositories_checked_in_percentage": "100.00%",
+ "repositories_checksummed_count": 20,
+ "repositories_checksum_failed_count": 5,
+ "repositories_checksummed_in_percentage": "48.78%",
+ "wikis_checksummed_count": 10,
+ "wikis_checksum_failed_count": 3,
+ "wikis_checksummed_in_percentage": "24.39%",
+ "repositories_verified_count": 20,
+ "repositories_verification_failed_count": 5,
+ "repositories_verified_in_percentage": "48.78%",
+ "repositories_checksum_mismatch_count": 3,
+ "wikis_verified_count": 10,
+ "wikis_verification_failed_count": 3,
+ "wikis_verified_in_percentage": "24.39%",
+ "wikis_checksum_mismatch_count": 1,
+ "repositories_retrying_verification_count": 1,
+ "wikis_retrying_verification_count": 3,
+ "repositories_checked_count": 7,
+ "repositories_checked_failed_count": 2,
+ "repositories_checked_in_percentage": "17.07%",
+ "last_event_id": 23,
+ "last_event_timestamp": 1509681166,
+ "cursor_last_event_id": nil,
+ "cursor_last_event_timestamp": 0,
+ "last_successful_status_check_timestamp": 1510125024,
+ "version": "10.3.0",
+ "revision": "33d33a096a",
+ },
+ {
+ "geo_node_id": 2,
+ "healthy": true,
+ "health": "Healthy",
+ "health_status": "Healthy",
+ "missing_oauth_application": false,
+ "attachments_count": 1,
+ "attachments_synced_count": 1,
+ "attachments_failed_count": 0,
+ "attachments_synced_missing_on_primary_count": 0,
+ "attachments_synced_in_percentage": "100.00%",
+ "db_replication_lag_seconds": 0,
+ "lfs_objects_count": 0,
+ "lfs_objects_synced_count": 0,
+ "lfs_objects_failed_count": 0,
+ "lfs_objects_synced_missing_on_primary_count": 0,
+ "lfs_objects_synced_in_percentage": "0.00%",
+ "job_artifacts_count": 2,
+ "job_artifacts_synced_count": 1,
+ "job_artifacts_failed_count": 1,
+ "job_artifacts_synced_missing_on_primary_count": 0,
+ "job_artifacts_synced_in_percentage": "50.00%",
+ "repositories_count": 41,
+ "projects_count": 41,
+ "repositories_failed_count": 1,
+ "repositories_synced_count": 40,
+ "repositories_synced_in_percentage": "97.56%",
+ "wikis_count": 41,
+ "wikis_failed_count": 0,
+ "wikis_synced_count": 41,
+ "wikis_synced_in_percentage": "100.00%",
+ "replication_slots_count": nil,
+ "replication_slots_used_count": nil,
+ "replication_slots_used_in_percentage": "0.00%",
+ "replication_slots_max_retained_wal_bytes": nil,
+ "repositories_checksummed_count": 20,
+ "repositories_checksum_failed_count": 5,
+ "repositories_checksummed_in_percentage": "48.78%",
+ "wikis_checksummed_count": 10,
+ "wikis_checksum_failed_count": 3,
+ "wikis_checksummed_in_percentage": "24.39%",
+ "repositories_verified_count": 20,
+ "repositories_verification_failed_count": 5,
+ "repositories_verified_in_percentage": "48.78%",
+ "repositories_checksum_mismatch_count": 3,
+ "wikis_verified_count": 10,
+ "wikis_verification_failed_count": 3,
+ "wikis_verified_in_percentage": "24.39%",
+ "wikis_checksum_mismatch_count": 1,
+ "repositories_retrying_verification_count": 4,
+ "wikis_retrying_verification_count": 2,
+ "repositories_checked_count": 5,
+ "repositories_checked_failed_count": 1,
+ "repositories_checked_in_percentage": "12.20%",
+ "last_event_id": 23,
+ "last_event_timestamp": 1509681166,
+ "cursor_last_event_id": 23,
+ "cursor_last_event_timestamp": 1509681166,
+ "last_successful_status_check_timestamp": 1510125024,
+ "version": "10.3.0",
+ "revision": "33d33a096a"
+ }
+]
+```
+
+Note: fields `wikis_count` and `repositories_count` are deprecated and will be deleted soon. Please use `projects_count` instead.
+
+## Retrieve status about a specific Geo node
+
+```
+GET /geo_nodes/:id/status
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/2/status
+```
+
+Example response:
+
+```json
+{
+ "geo_node_id": 2,
+ "healthy": true,
+ "health": "Healthy",
+ "health_status": "Healthy",
+ "missing_oauth_application": false,
+ "attachments_count": 1,
+ "attachments_synced_count": 1,
+ "attachments_failed_count": 0,
+ "attachments_synced_missing_on_primary_count": 0,
+ "attachments_synced_in_percentage": "100.00%",
+ "db_replication_lag_seconds": 0,
+ "lfs_objects_count": 0,
+ "lfs_objects_synced_count": 0,
+ "lfs_objects_failed_count": 0,
+ "lfs_objects_synced_missing_on_primary_count": 0,
+ "lfs_objects_synced_in_percentage": "0.00%",
+ "job_artifacts_count": 2,
+ "job_artifacts_synced_count": 1,
+ "job_artifacts_failed_count": 1,
+ "job_artifacts_synced_missing_on_primary_count": 0,
+ "job_artifacts_synced_in_percentage": "50.00%",
+ "repositories_count": 41,
+ "projects_count": 41,
+ "repositories_failed_count": 1,
+ "repositories_synced_count": 40,
+ "repositories_synced_in_percentage": "97.56%",
+ "wikis_count": 41,
+ "wikis_failed_count": 0,
+ "wikis_synced_count": 41,
+ "wikis_synced_in_percentage": "100.00%",
+ "replication_slots_count": nil,
+ "replication_slots_used_count": nil,
+ "replication_slots_used_in_percentage": "0.00%",
+ "replication_slots_max_retained_wal_bytes": nil,
+ "last_event_id": 23,
+ "last_event_timestamp": 1509681166,
+ "cursor_last_event_id": 23,
+ "cursor_last_event_timestamp": 1509681166,
+ "last_successful_status_check_timestamp": 1510125268,
+ "version": "10.3.0",
+ "revision": "33d33a096a"
+}
+```
+
+Note: The `health_status` parameter can only be in an "Healthy" or "Unhealthy" state, while the `health` parameter can be empty, "Healthy", or contain the actual error message.
+
+Note: Fields `wikis_count` and `repositories_count` are deprecated and will be deleted soon. Please use `projects_count` instead.
+
+## Retrieve project sync or verification failures that occurred on the current node
+
+This only works on a secondary node.
+
+```
+GET /geo_nodes/current/failures
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `type` | string | no | Type of failed objects (`repository`/`wiki`) |
+| `failure_type` | string | no | Type of failures (`sync`/`checksum_mismatch`/`verification`) |
+
+This endpoint uses [Pagination](README.md#pagination).
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/current/failures
+```
+
+Example response:
+
+```json
+[
+ {
+ "project_id": 3,
+ "last_repository_synced_at": "2017-10-31 14:25:55 UTC",
+ "last_repository_successful_sync_at": "2017-10-31 14:26:04 UTC",
+ "last_wiki_synced_at": "2017-10-31 14:26:04 UTC",
+ "last_wiki_successful_sync_at": "2017-10-31 14:26:11 UTC",
+ "repository_retry_count": null,
+ "wiki_retry_count": 1,
+ "last_repository_sync_failure": null,
+ "last_wiki_sync_failure": "Error syncing Wiki repository",
+ "last_repository_verification_failure": "",
+ "last_wiki_verification_failure": "",
+ "repository_verification_checksum_sha": "da39a3ee5e6b4b0d32e5bfef9a601890afd80709",
+ "wiki_verification_checksum_sha": "da39a3ee5e6b4b0d3255bfef9ef0189aafd80709",
+ "repository_checksum_mismatch": false,
+ "wiki_checksum_mismatch": false
+ }
+]
+```
diff --git a/doc/api/issue_links.md b/doc/api/issue_links.md
new file mode 100644
index 00000000000..1c7db6a8e4c
--- /dev/null
+++ b/doc/api/issue_links.md
@@ -0,0 +1,215 @@
+# Issue links API **[STARTER]**
+
+## List issue relations
+
+Get a list of related issues of a given issue, sorted by the relationship creation datetime (ascending).
+Issues will be filtered according to the user authorizations.
+
+```
+GET /projects/:id/issues/:issue_iid/links
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|--------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+
+```json
+[
+ {
+ "id" : 84,
+ "iid" : 14,
+ "issue_link_id": 1
+ "project_id" : 4,
+ "created_at" : "2016-01-07T12:44:33.959Z",
+ "title" : "Issues with auth",
+ "state" : "opened",
+ "assignees" : [],
+ "assignee" : null,
+ "labels" : [
+ "bug"
+ ],
+ "author" : {
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/eileen.lowe",
+ "id" : 18,
+ "username" : "eileen.lowe"
+ },
+ "description" : null,
+ "updated_at" : "2016-01-07T12:44:33.959Z",
+ "milestone" : null,
+ "subscribed" : true,
+ "user_notes_count": 0,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/14",
+ "confidential": false,
+ "weight": null,
+ }
+]
+```
+
+## Create an issue link
+
+Creates a two-way relation between two issues. User must be allowed to update both issues in order to succeed.
+
+```
+POST /projects/:id/issues/:issue_iid/links
+```
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|--------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+| `target_project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) of a target project |
+| `target_issue_iid` | integer/string | yes | The internal ID of a target project's issue |
+
+
+```json
+{
+ "source_issue" : {
+ "id" : 83,
+ "iid" : 11,
+ "project_id" : 4,
+ "created_at" : "2016-01-07T12:44:33.959Z",
+ "title" : "Issues with auth",
+ "state" : "opened",
+ "assignees" : [],
+ "assignee" : null,
+ "labels" : [
+ "bug"
+ ],
+ "author" : {
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/eileen.lowe",
+ "id" : 18,
+ "username" : "eileen.lowe"
+ },
+ "description" : null,
+ "updated_at" : "2016-01-07T12:44:33.959Z",
+ "milestone" : null,
+ "subscribed" : true,
+ "user_notes_count": 0,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/11",
+ "confidential": false,
+ "weight": null,
+ },
+ "target_issue" : {
+ "id" : 84,
+ "iid" : 14,
+ "project_id" : 4,
+ "created_at" : "2016-01-07T12:44:33.959Z",
+ "title" : "Issues with auth",
+ "state" : "opened",
+ "assignees" : [],
+ "assignee" : null,
+ "labels" : [
+ "bug"
+ ],
+ "author" : {
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/eileen.lowe",
+ "id" : 18,
+ "username" : "eileen.lowe"
+ },
+ "description" : null,
+ "updated_at" : "2016-01-07T12:44:33.959Z",
+ "milestone" : null,
+ "subscribed" : true,
+ "user_notes_count": 0,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/14",
+ "confidential": false,
+ "weight": null,
+ }
+}
+```
+
+## Delete an issue link
+
+Deletes an issue link, thus removes the two-way relationship.
+
+```
+DELETE /projects/:id/issues/:issue_iid/links/:issue_link_id
+```
+
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|--------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `issue_iid` | integer | yes | The internal ID of a project's issue |
+| `issue_link_id` | integer/string | yes | The ID of an issue relationship |
+
+
+```json
+{
+ "source_issue" : {
+ "id" : 83,
+ "iid" : 11,
+ "project_id" : 4,
+ "created_at" : "2016-01-07T12:44:33.959Z",
+ "title" : "Issues with auth",
+ "state" : "opened",
+ "assignees" : [],
+ "assignee" : null,
+ "labels" : [
+ "bug"
+ ],
+ "author" : {
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/eileen.lowe",
+ "id" : 18,
+ "username" : "eileen.lowe"
+ },
+ "description" : null,
+ "updated_at" : "2016-01-07T12:44:33.959Z",
+ "milestone" : null,
+ "subscribed" : true,
+ "user_notes_count": 0,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/11",
+ "confidential": false,
+ "weight": null,
+ },
+ "target_issue" : {
+ "id" : 84,
+ "iid" : 14,
+ "project_id" : 4,
+ "created_at" : "2016-01-07T12:44:33.959Z",
+ "title" : "Issues with auth",
+ "state" : "opened",
+ "assignees" : [],
+ "assignee" : null,
+ "labels" : [
+ "bug"
+ ],
+ "author" : {
+ "name" : "Alexandra Bashirian",
+ "avatar_url" : null,
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/eileen.lowe",
+ "id" : 18,
+ "username" : "eileen.lowe"
+ },
+ "description" : null,
+ "updated_at" : "2016-01-07T12:44:33.959Z",
+ "milestone" : null,
+ "subscribed" : true,
+ "user_notes_count": 0,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/14",
+ "confidential": false,
+ "weight": null,
+ }
+}
+```
diff --git a/doc/api/issues.md b/doc/api/issues.md
index cb5789e76b7..4fb3626f637 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -37,23 +37,26 @@ GET /issues?confidential=true
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
+| `state` | string | no | Return `all` issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
+| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
-| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search issues against their `title` and `description` |
-| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
+| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
| `created_after` | datetime | no | Return issues created on or after the given time |
| `created_before` | datetime | no | Return issues created on or before the given time |
| `updated_after` | datetime | no | Return issues updated on or after the given time |
| `updated_before` | datetime | no | Return issues updated on or before the given time |
-| `confidential ` | Boolean | no | Filter confidential or public issues. |
+| `confidential ` | Boolean | no | Filter confidential or public issues. |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/issues
@@ -109,7 +112,7 @@ Example response:
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z",
"iid" : 6,
- "labels" : [],
+ "labels" : ["foo", "bar"],
"upvotes": 4,
"downvotes": 0,
"merge_requests_count": 0,
@@ -122,8 +125,17 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
+ "has_tasks": true,
+ "task_status": "10 of 15 tasks completed",
"confidential": false,
- "discussion_locked": false
+ "discussion_locked": false,
+ "_links":{
+ "self":"http://example.com/api/v4/projects/1/issues/76",
+ "notes":"`http://example.com/`api/v4/projects/1/issues/76/notes",
+ "award_emoji":"http://example.com/api/v4/projects/1/issues/76/award_emoji",
+ "project":"http://example.com/api/v4/projects/1"
+ },
+ "subscribed": false
}
]
```
@@ -158,11 +170,14 @@ GET /groups/:id/issues?confidential=true
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
+| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
-| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
@@ -221,7 +236,7 @@ Example response:
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
- "labels" : [],
+ "labels" : ["foo", "bar"],
"upvotes": 4,
"downvotes": 0,
"merge_requests_count": 0,
@@ -240,8 +255,17 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
+ "has_tasks": true,
+ "task_status": "10 of 15 tasks completed",
"confidential": false,
- "discussion_locked": false
+ "discussion_locked": false,
+ "_links":{
+ "self":"http://example.com/api/v4/projects/4/issues/41",
+ "notes":"`http://example.com/`api/v4/projects/4/issues/41/notes",
+ "award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji",
+ "project":"http://example.com/api/v4/projects/4"
+ },
+ "subscribed": false
}
]
```
@@ -277,10 +301,13 @@ GET /projects/:id/issues?confidential=true
| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
+| `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
-| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
@@ -340,7 +367,7 @@ Example response:
"id" : 9,
"name" : "Dr. Luella Kovacek"
},
- "labels" : [],
+ "labels" : ["foo", "bar"],
"upvotes": 4,
"downvotes": 0,
"merge_requests_count": 0,
@@ -366,8 +393,17 @@ Example response:
"human_time_estimate": null,
"human_total_time_spent": null
},
+ "has_tasks": true,
+ "task_status": "10 of 15 tasks completed",
"confidential": false,
- "discussion_locked": false
+ "discussion_locked": false,
+ "_links":{
+ "self":"http://example.com/api/v4/projects/4/issues/41",
+ "notes":"`http://example.com/`api/v4/projects/4/issues/41/notes",
+ "award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji",
+ "project":"http://example.com/api/v4/projects/4"
+ },
+ "subscribed": false
}
]
```
diff --git a/doc/api/issues_statistics.md b/doc/api/issues_statistics.md
new file mode 100644
index 00000000000..82bc9c142cc
--- /dev/null
+++ b/doc/api/issues_statistics.md
@@ -0,0 +1,177 @@
+# Issues Statistics API
+
+Every API call to issues_statistics must be authenticated.
+
+If a user is not a member of a project and the project is private, a `GET`
+request on that project will result to a `404` status code.
+
+## Get issues statistics
+
+Gets issues count statistics on all issues the authenticated user has access to. By default it
+returns only issues created by the current user. To get all issues,
+use parameter `scope=all`.
+
+```
+GET /issues_statistics
+GET /issues_statistics?labels=foo
+GET /issues_statistics?labels=foo,bar
+GET /issues_statistics?labels=foo,bar&state=opened
+GET /issues_statistics?milestone=1.0.0
+GET /issues_statistics?milestone=1.0.0&state=opened
+GET /issues_statistics?iids[]=42&iids[]=43
+GET /issues_statistics?author_id=5
+GET /issues_statistics?assignee_id=5
+GET /issues_statistics?my_reaction_emoji=star
+GET /issues_statistics?search=foo&in=title
+GET /issues_statistics?confidential=true
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. |
+| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
+| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me` |
+| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
+| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
+| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
+| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
+| `search` | string | no | Search issues against their `title` and `description` |
+| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
+| `created_after` | datetime | no | Return issues created on or after the given time |
+| `created_before` | datetime | no | Return issues created on or before the given time |
+| `updated_after` | datetime | no | Return issues updated on or after the given time |
+| `updated_before` | datetime | no | Return issues updated on or before the given time |
+| `confidential ` | Boolean | no | Filter confidential or public issues. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/issues_statistics
+```
+
+Example response:
+
+```json
+{
+ "statistics": {
+ "counts": {
+ "all": 20,
+ "closed": 5,
+ "opened": 15
+ }
+ }
+}
+```
+
+## Get group issues statistics
+
+Gets issues count statistics for given group.
+
+```
+GET /groups/:id/issues_statistics
+GET /groups/:id/issues_statistics?labels=foo
+GET /groups/:id/issues_statistics?labels=foo,bar
+GET /groups/:id/issues_statistics?labels=foo,bar&state=opened
+GET /groups/:id/issues_statistics?milestone=1.0.0
+GET /groups/:id/issues_statistics?milestone=1.0.0&state=opened
+GET /groups/:id/issues_statistics?iids[]=42&iids[]=43
+GET /groups/:id/issues_statistics?search=issue+title+or+description
+GET /groups/:id/issues_statistics?author_id=5
+GET /groups/:id/issues_statistics?assignee_id=5
+GET /groups/:id/issues_statistics?my_reaction_emoji=star
+GET /groups/:id/issues_statistics?confidential=true
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. |
+| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
+| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
+| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. |
+| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
+| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
+| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
+| `search` | string | no | Search group issues against their `title` and `description` |
+| `created_after` | datetime | no | Return issues created on or after the given time |
+| `created_before` | datetime | no | Return issues created on or before the given time |
+| `updated_after` | datetime | no | Return issues updated on or after the given time |
+| `updated_before` | datetime | no | Return issues updated on or before the given time |
+| `confidential ` | Boolean | no | Filter confidential or public issues. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/4/issues_statistics
+```
+
+Example response:
+
+```json
+{
+ "statistics": {
+ "counts": {
+ "all": 20,
+ "closed": 5,
+ "opened": 15
+ }
+ }
+}
+```
+
+## Get project issues statistics
+
+Gets issues count statistics for given project.
+
+```
+GET /projects/:id/issues_statistics
+GET /projects/:id/issues_statistics?labels=foo
+GET /projects/:id/issues_statistics?labels=foo,bar
+GET /projects/:id/issues_statistics?labels=foo,bar&state=opened
+GET /projects/:id/issues_statistics?milestone=1.0.0
+GET /projects/:id/issues_statistics?milestone=1.0.0&state=opened
+GET /projects/:id/issues_statistics?iids[]=42&iids[]=43
+GET /projects/:id/issues_statistics?search=issue+title+or+description
+GET /projects/:id/issues_statistics?author_id=5
+GET /projects/:id/issues_statistics?assignee_id=5
+GET /projects/:id/issues_statistics?my_reaction_emoji=star
+GET /projects/:id/issues_statistics?confidential=true
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. |
+| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
+| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. |
+| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
+| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
+| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
+| `search` | string | no | Search project issues against their `title` and `description` |
+| `created_after` | datetime | no | Return issues created on or after the given time |
+| `created_before` | datetime | no | Return issues created on or before the given time |
+| `updated_after` | datetime | no | Return issues updated on or after the given time |
+| `updated_before` | datetime | no | Return issues updated on or before the given time |
+| `confidential ` | Boolean | no | Filter confidential or public issues. |
+
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues_statistics
+```
+
+Example response:
+
+```json
+{
+ "statistics": {
+ "counts": {
+ "all": 20,
+ "closed": 5,
+ "opened": 15
+ }
+ }
+}
+```
diff --git a/doc/api/license.md b/doc/api/license.md
new file mode 100644
index 00000000000..2a8de64bdbf
--- /dev/null
+++ b/doc/api/license.md
@@ -0,0 +1,176 @@
+# License **[CORE ONLY]**
+
+In order to interact with license endpoints, you need to authenticate yourself
+as an admin.
+
+## Retrieve information about the current license
+
+```
+GET /license
+```
+
+```json
+{
+ "id": 2,
+ "plan": "gold",
+ "created_at": "2018-02-27T23:21:58.674Z",
+ "starts_at": "2018-01-27",
+ "expires_at": "2022-01-27",
+ "historical_max": 300,
+ "expired": false,
+ "overage": 200,
+ "user_limit": 100,
+ "active_users": 300,
+ "licensee": {
+ "Name": "John Doe1"
+ },
+ "add_ons": {
+ "GitLab_FileLocks": 1,
+ "GitLab_Auditor_User": 1
+ }
+}
+```
+
+## Retrieve information about all licenses
+
+```
+GET /licenses
+```
+
+```json
+[
+ {
+ "id": 1,
+ "plan": "silver",
+ "created_at": "2018-02-27T23:21:58.674Z",
+ "starts_at": "2018-01-27",
+ "expires_at": "2022-01-27",
+ "historical_max": 300,
+ "expired": false,
+ "overage": 200,
+ "user_limit": 100,
+ "licensee": {
+ "Name": "John Doe1"
+ },
+ "add_ons": {
+ "GitLab_FileLocks": 1,
+ "GitLab_Auditor_User": 1
+ }
+ },
+ {
+ "id": 2,
+ "plan": "gold",
+ "created_at": "2018-02-27T23:21:58.674Z",
+ "starts_at": "2018-01-27",
+ "expires_at": "2022-01-27",
+ "historical_max": 300,
+ "expired": false,
+ "overage": 200,
+ "user_limit": 100,
+ "licensee": {
+ "Name": "Doe John"
+ },
+ "add_ons": {
+ "GitLab_FileLocks": 1,
+ }
+ }
+]
+```
+
+Overage is the difference between the number of active users and the licensed number of users.
+This is calculated differently depending on whether the license has expired or not.
+
+- If the license has expired, it uses the historical maximum active user count (`historical_max`).
+- If the license has not expired, it uses the current active users count.
+
+Returns:
+
+- `200 OK` with response containing the licenses in JSON format. This will be an empty JSON array if there are no licenses.
+- `403 Forbidden` if the current user in not permitted to read the licenses.
+
+## Add a new license
+
+```
+POST /license
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `license` | string | yes | The license string |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/license?license=eyJkYXRhIjoiMHM5Q...S01Udz09XG4ifQ=="
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "plan": "gold",
+ "created_at": "2018-02-27T23:21:58.674Z",
+ "starts_at": "2018-01-27",
+ "expires_at": "2022-01-27",
+ "historical_max": 300,
+ "expired": false,
+ "overage": 200,
+ "user_limit": 100,
+ "active_users": 300,
+ "licensee": {
+ "Name": "John Doe1"
+ },
+ "add_ons": {
+ "GitLab_FileLocks": 1,
+ "GitLab_Auditor_User": 1
+ }
+}
+```
+
+Returns:
+
+- `201 Created` if the license is successfully added.
+- `400 Bad Request` if the license couldn't be added, with an error message explaining the reason.
+
+
+## Delete a license
+
+```
+DELETE /license/:id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | ID of the GitLab license. |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/license/:id"
+```
+
+Example response:
+
+```json
+{
+ "id": 2,
+ "plan": "gold",
+ "created_at": "2018-02-27T23:21:58.674Z",
+ "starts_at": "2018-01-27",
+ "expires_at": "2022-01-27",
+ "historical_max": 300,
+ "expired": false,
+ "overage": 200,
+ "user_limit": 100,
+ "licensee": {
+ "Name": "John Doe"
+ },
+ "add_ons": {
+ "GitLab_FileLocks": 1,
+ "GitLab_Auditor_User": 1
+ }
+}
+```
+
+Returns:
+
+- `204 No Content` if the license is successfully deleted.
+- `403 Forbidden` if the current user in not permitted to delete the license.
+- `404 Not Found` if the license to delete could not be found.
diff --git a/doc/api/license_templates.md b/doc/api/license_templates.md
new file mode 100644
index 00000000000..1b68af9ce31
--- /dev/null
+++ b/doc/api/license_templates.md
@@ -0,0 +1,5 @@
+---
+redirect_to: 'templates/licenses.md'
+---
+
+This document was moved to [another location](templates/licenses.md).
diff --git a/doc/api/managed_licenses.md b/doc/api/managed_licenses.md
new file mode 100644
index 00000000000..47b193111b6
--- /dev/null
+++ b/doc/api/managed_licenses.md
@@ -0,0 +1,136 @@
+# Managed Licenses API **[ULTIMATE]**
+
+## List managed licenses
+
+Get all managed licenses for a given project.
+
+```
+GET /projects/:id/managed_licenses
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/managed_licenses
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "name": "MIT",
+ "approval_status": "approved"
+ },
+ {
+ "id": 3,
+ "name": "ISC",
+ "approval_status": "blacklisted"
+ }
+]
+```
+
+## Show an existing managed license
+
+Shows an existing managed license.
+
+```
+GET /projects/:id/managed_licenses/:managed_license_id
+```
+
+| Attribute | Type | Required | Description |
+| --------------- | ------- | --------------------------------- | ------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses/6"
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "name": "MIT",
+ "approval_status": "blacklisted"
+}
+```
+
+## Create a new managed license
+
+Creates a new managed license for the given project with the given name and approval status.
+
+```
+POST /projects/:id/managed_licenses
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | ------- | -------- | ---------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `name` | string | yes | The name of the managed license |
+| `approval_status` | string | yes | The approval status. "approved" or "blacklisted" |
+
+```bash
+curl --data "name=MIT&approval_status=blacklisted" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses"
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "name": "MIT",
+ "approval_status": "approved"
+}
+```
+
+## Delete a managed license
+
+Deletes a managed license with a given id.
+
+```
+DELETE /projects/:id/managed_licenses/:managed_license_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses/4"
+```
+
+When successful, it replies with an HTTP 204 response.
+
+## Edit an existing managed license
+
+Updates an existing managed license with a new approval status.
+
+```
+PATCH /projects/:id/managed_licenses/:managed_license_id
+```
+
+| Attribute | Type | Required | Description |
+| --------------- | ------- | --------------------------------- | ------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `managed_license_id` | integer/string | yes | The ID or URL-encoded name of the license belonging to the project |
+| `approval_status` | string | yes | The approval status. "approved" or "blacklisted" |
+
+```bash
+curl --request PATCH --data "approval_status=blacklisted" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/managed_licenses/6"
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "name": "MIT",
+ "approval_status": "blacklisted"
+}
+```
diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md
new file mode 100644
index 00000000000..ddac81328b9
--- /dev/null
+++ b/doc/api/merge_request_approvals.md
@@ -0,0 +1,425 @@
+# Merge request approvals API **[STARTER]**
+
+Configuration for approvals on all Merge Requests (MR) in the project. Must be authenticated for all endpoints.
+
+## Project-level MR approvals
+
+### Get Configuration
+
+>**Note:** This API endpoint is only available on 10.6 Starter and above.
+
+You can request information about a project's approval configuration using the
+following endpoint:
+
+```
+GET /projects/:id/approvals
+```
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | ------------------- |
+| `id` | integer | yes | The ID of a project |
+
+```json
+{
+ "approvers": [
+ {
+ "user": {
+ "id": 5,
+ "name": "John Doe6",
+ "username": "user5",
+ "state":"active","avatar_url":"https://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80\u0026d=identicon","web_url":"http://localhost/user5"
+ }
+ }
+ ],
+ "approver_groups": [
+ {
+ "group": {
+ "id": 1,
+ "name": "group1",
+ "path": "group1",
+ "description": "",
+ "visibility": "public",
+ "lfs_enabled": false,
+ "avatar_url": null,
+ "web_url": "http://localhost/groups/group1",
+ "request_access_enabled": false,
+ "full_name": "group1",
+ "full_path": "group1",
+ "parent_id": null,
+ "ldap_cn": null,
+ "ldap_access": null
+ }
+ }
+ ],
+ "approvals_before_merge": 2,
+ "reset_approvals_on_push": true,
+ "disable_overriding_approvers_per_merge_request": false
+}
+```
+
+### Change configuration
+
+>**Note:** This API endpoint is only available on 10.6 Starter and above.
+
+If you are allowed to, you can change approval configuration using the following
+endpoint:
+
+```
+POST /projects/:id/approvals
+```
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+| ------------------------------------------------ | ------- | -------- | ---------------------------------------------------------- |
+| `id` | integer | yes | The ID of a project |
+| `approvals_before_merge` | integer | no | How many approvals are required before an MR can be merged |
+| `reset_approvals_on_push` | boolean | no | Reset approvals on a new push |
+| `disable_overriding_approvers_per_merge_request` | boolean | no | Allow/Disallow overriding approvers per MR |
+| `merge_requests_author_approval` | boolean | no | Allow/Disallow authors be able to self approve merge requests |
+
+```json
+{
+ "approvers": [
+ {
+ "user": {
+ "id": 5,
+ "name": "John Doe6",
+ "username": "user5",
+ "state":"active","avatar_url":"https://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80\u0026d=identicon","web_url":"http://localhost/user5"
+ }
+ }
+ ],
+ "approver_groups": [
+ {
+ "group": {
+ "id": 1,
+ "name": "group1",
+ "path": "group1",
+ "description": "",
+ "visibility": "public",
+ "lfs_enabled": false,
+ "avatar_url": null,
+ "web_url": "http://localhost/groups/group1",
+ "request_access_enabled": false,
+ "full_name": "group1",
+ "full_path": "group1",
+ "parent_id": null,
+ "ldap_cn": null,
+ "ldap_access": null
+ }
+ }
+ ],
+ "approvals_before_merge": 2,
+ "reset_approvals_on_push": true,
+ "disable_overriding_approvers_per_merge_request": false,
+ "merge_requests_author_approval": false
+}
+```
+
+### Change allowed approvers
+
+>**Note:** This API endpoint is only available on 10.6 Starter and above.
+
+If you are allowed to, you can change approvers and approver groups using
+the following endpoint:
+
+```
+PUT /projects/:id/approvers
+```
+
+**Important:** Approvers and groups not in the request will be **removed**
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+| -------------------- | ------- | -------- | --------------------------------------------------- |
+| `id` | integer | yes | The ID of a project |
+| `approver_ids` | Array | yes | An array of User IDs that can approve MRs |
+| `approver_group_ids` | Array | yes | An array of Group IDs whose members can approve MRs |
+
+```json
+{
+ "approvers": [
+ {
+ "user": {
+ "id": 5,
+ "name": "John Doe6",
+ "username": "user5",
+ "state":"active","avatar_url":"https://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80\u0026d=identicon","web_url":"http://localhost/user5"
+ }
+ }
+ ],
+ "approver_groups": [
+ {
+ "group": {
+ "id": 1,
+ "name": "group1",
+ "path": "group1",
+ "description": "",
+ "visibility": "public",
+ "lfs_enabled": false,
+ "avatar_url": null,
+ "web_url": "http://localhost/groups/group1",
+ "request_access_enabled": false,
+ "full_name": "group1",
+ "full_path": "group1",
+ "parent_id": null,
+ "ldap_cn": null,
+ "ldap_access": null
+ }
+ }
+ ],
+ "approvals_before_merge": 2,
+ "reset_approvals_on_push": true,
+ "disable_overriding_approvers_per_merge_request": false
+}
+```
+
+
+## Merge Request-level MR approvals
+
+Configuration for approvals on a specific Merge Request. Must be authenticated for all endpoints.
+
+### Get Configuration
+
+>**Note:** This API endpoint is only available on 8.9 Starter and above.
+
+You can request information about a merge request's approval status using the
+following endpoint:
+
+```
+GET /projects/:id/merge_requests/:merge_request_iid/approvals
+```
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|---------------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The IID of MR |
+
+```json
+{
+ "id": 5,
+ "iid": 5,
+ "project_id": 1,
+ "title": "Approvals API",
+ "description": "Test",
+ "state": "opened",
+ "created_at": "2016-06-08T00:19:52.638Z",
+ "updated_at": "2016-06-08T21:20:42.470Z",
+ "merge_status": "cannot_be_merged",
+ "approvals_required": 2,
+ "approvals_left": 1,
+ "approved_by": [
+ {
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
+ "web_url": "http://localhost:3000/u/root"
+ }
+ }
+ ],
+ "approvers": [],
+ "approver_groups": []
+}
+```
+
+### Change approval configuration
+
+>**Note:** This API endpoint is only available on 10.6 Starter and above.
+
+If you are allowed to, you can change `approvals_required` using the following
+endpoint:
+
+```
+POST /projects/:id/merge_requests/:merge_request_iid/approvals
+```
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|----------------------|---------|----------|--------------------------------------------|
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The IID of MR |
+| `approvals_required` | integer | yes | Approvals required before MR can be merged |
+
+
+```json
+{
+ "id": 5,
+ "iid": 5,
+ "project_id": 1,
+ "title": "Approvals API",
+ "description": "Test",
+ "state": "opened",
+ "created_at": "2016-06-08T00:19:52.638Z",
+ "updated_at": "2016-06-08T21:20:42.470Z",
+ "merge_status": "cannot_be_merged",
+ "approvals_required": 2,
+ "approvals_left": 2,
+ "approved_by": [],
+ "approvers": [],
+ "approver_groups": []
+}
+```
+
+### Change allowed approvers for Merge Request
+
+>**Note:** This API endpoint is only available on 10.6 Starter and above.
+
+If you are allowed to, you can change approvers and approver groups using
+the following endpoint:
+
+```
+PUT /projects/:id/merge_requests/:merge_request_iid/approvers
+```
+
+**Important:** Approvers and groups not in the request will be **removed**
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|----------------------|---------|----------|-----------------------------------------------------------|
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The IID of MR |
+| `approver_ids` | Array | yes | An array of User IDs that can approve the MR |
+| `approver_group_ids` | Array | yes | An array of Group IDs whose members can approve the MR |
+
+```json
+{
+ "id": 5,
+ "iid": 5,
+ "project_id": 1,
+ "title": "Approvals API",
+ "description": "Test",
+ "state": "opened",
+ "created_at": "2016-06-08T00:19:52.638Z",
+ "updated_at": "2016-06-08T21:20:42.470Z",
+ "merge_status": "cannot_be_merged",
+ "approvals_required": 2,
+ "approvals_left": 2,
+ "approved_by": [],
+ "approvers": [
+ {
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
+ "web_url": "http://localhost:3000/u/root"
+ }
+ }
+ ],
+ "approver_groups": [
+ {
+ "group": {
+ "id": 5,
+ "name": "group1",
+ "path": "group1",
+ "description": "",
+ "visibility": "public",
+ "lfs_enabled": false,
+ "avatar_url": null,
+ "web_url": "http://localhost/groups/group1",
+ "request_access_enabled": false,
+ "full_name": "group1",
+ "full_path": "group1",
+ "parent_id": null,
+ "ldap_cn": null,
+ "ldap_access": null
+ }
+ }
+ ]
+}
+```
+
+## Approve Merge Request
+
+>**Note:** This API endpoint is only available on 8.9 Starter and above.
+
+If you are allowed to, you can approve a merge request using the following
+endpoint:
+
+```
+POST /projects/:id/merge_requests/:merge_request_iid/approve
+```
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|---------------------|---------|----------|-------------------------|
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The IID of MR |
+| `sha` | string | no | The HEAD of the MR |
+| `approval_password` **[STARTER]** | string | no | Current user's password. Required if [**Require user password to approve**](../user/project/merge_requests/merge_request_approvals.md#require-authentication-when-approving-a-merge-request-starter) is enabled in the project settings. |
+
+The `sha` parameter works in the same way as
+when [accepting a merge request](merge_requests.md#accept-mr): if it is passed, then it must
+match the current HEAD of the merge request for the approval to be added. If it
+does not match, the response code will be `409`.
+
+```json
+{
+ "id": 5,
+ "iid": 5,
+ "project_id": 1,
+ "title": "Approvals API",
+ "description": "Test",
+ "state": "opened",
+ "created_at": "2016-06-08T00:19:52.638Z",
+ "updated_at": "2016-06-09T21:32:14.105Z",
+ "merge_status": "can_be_merged",
+ "approvals_required": 2,
+ "approvals_left": 0,
+ "approved_by": [
+ {
+ "user": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
+ "web_url": "http://localhost:3000/u/root"
+ }
+ },
+ {
+ "user": {
+ "name": "Nico Cartwright",
+ "username": "ryley",
+ "id": 2,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/cf7ad14b34162a76d593e3affca2adca?s=80\u0026d=identicon",
+ "web_url": "http://localhost:3000/u/ryley"
+ }
+ }
+ ],
+ "approvers": [],
+ "approver_groups": []
+}
+```
+
+## Unapprove Merge Request
+
+>**Note:** This API endpoint is only available on 9.0 Starter and above.
+
+If you did approve a merge request, you can unapprove it using the following
+endpoint:
+
+```
+POST /projects/:id/merge_requests/:merge_request_iid/unapprove
+```
+
+**Parameters:**
+
+| Attribute | Type | Required | Description |
+|---------------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `merge_request_iid` | integer | yes | The IID of MR |
diff --git a/doc/api/packages.md b/doc/api/packages.md
new file mode 100644
index 00000000000..618e5c3056a
--- /dev/null
+++ b/doc/api/packages.md
@@ -0,0 +1,152 @@
+# Packages API **[PREMIUM]**
+
+This is the API docs of [GitLab Packages](../administration/packages.md).
+
+## List project packages
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9259) in GitLab 11.8.
+
+Get a list of project packages. Both Maven and NPM packages are included in results.
+When accessed without authentication, only packages of public projects are returned.
+
+```
+GET /projects/:id/packages
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/packages
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "name": "com/mycompany/my-app",
+ "version": "1.0-SNAPSHOT",
+ "package_type": "maven"
+ },
+ {
+ "id": 2,
+ "name": "@foo/bar",
+ "version": "1.0.3",
+ "package_type": "npm"
+ }
+]
+```
+
+By default, the `GET` request will return 20 results, since the API is [paginated](README.md#pagination).
+
+## Get a project package
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9667) in GitLab 11.9.
+
+Get a single project package.
+
+```
+GET /projects/:id/packages/:package_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+| `package_id` | integer | yes | ID of a package. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/packages/:package_id
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "name": "com/mycompany/my-app",
+ "version": "1.0-SNAPSHOT",
+ "package_type": "maven"
+}
+```
+
+## List package files
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9305) in GitLab 11.8.
+
+Get a list of package files of a single package.
+
+```
+GET /projects/:id/packages/:package_id/package_files
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `package_id` | integer | yes | ID of a package. |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/packages/4/package_files
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 25,
+ "package_id": 4,
+ "created_at": "2018-11-07T15:25:52.199Z",
+ "file_name": "my-app-1.5-20181107.152550-1.jar",
+ "size": 2421,
+ "file_md5": "58e6a45a629910c6ff99145a688971ac",
+ "file_sha1": "ebd193463d3915d7e22219f52740056dfd26cbfe"
+ },
+ {
+ "id": 26,
+ "package_id": 4,
+ "created_at": "2018-11-07T15:25:56.776Z",
+ "file_name": "my-app-1.5-20181107.152550-1.pom",
+ "size": 1122,
+ "file_md5": "d90f11d851e17c5513586b4a7e98f1b2",
+ "file_sha1": "9608d068fe88aff85781811a42f32d97feb440b5"
+ },
+ {
+ "id": 27,
+ "package_id": 4,
+ "created_at": "2018-11-07T15:26:00.556Z",
+ "file_name": "maven-metadata.xml",
+ "size": 767,
+ "file_md5": "6dfd0cce1203145a927fef5e3a1c650c",
+ "file_sha1": "d25932de56052d320a8ac156f745ece73f6a8cd2"
+ }
+]
+```
+
+By default, the `GET` request will return 20 results, since the API is [paginated](README.md#pagination).
+
+## Delete a project package
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9623) in GitLab 11.9.
+
+Deletes a project package.
+
+```
+DELETE /projects/:id/packages/:package_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `package_id` | integer | yes | ID of a package. |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/packages/:package_id
+```
+
+Can return the following status codes:
+
+- `204 No Content`, if the package was deleted successfully.
+- `404 Not Found`, if the package was not found.
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 6fcc06ea8cd..87c7f371de1 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -181,7 +181,7 @@ Currently gitlab-shell has a boolean return code, preventing GitLab from specify
## Delete existing file in repository
-This allows you to delete a single file. For deleting multiple files with a singleh request see the [commits API](commits.html#create-a-commit-with-multiple-files-and-actions).
+This allows you to delete a single file. For deleting multiple files with a single request, see the [commits API](commits.html#create-a-commit-with-multiple-files-and-actions).
```
DELETE /projects/:id/repository/files/:file_path
diff --git a/doc/api/scim.md b/doc/api/scim.md
new file mode 100644
index 00000000000..4595c6f2ed3
--- /dev/null
+++ b/doc/api/scim.md
@@ -0,0 +1,235 @@
+# SCIM API
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
+
+The SCIM API implements the [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
+
+NOTE: **Note:**
+[Group SSO](../user/group/saml_sso/index.md) and the feature
+flag `:group_scim` must be enabled for the group. For more information, see [SCIM setup documentation](../user/group/saml_sso/scim_setup.md#requirements).
+
+## Get a list of SAML users
+
+NOTE: **Note:**
+This endpoint is used as part of the SCIM syncing mechanism and it only returns
+a single user based on a unique ID which should match the `extern_uid` of the user.
+
+```text
+GET /api/scim/v2/groups/:group_path/Users
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------|
+| `filter` | string | yes | A [filter](#available-filters) expression. |
+| `group_path` | string | yes | Full path to the group. |
+
+Example request:
+
+```sh
+curl 'https://example.gitlab.com/api/scim/v2/groups/test_group/Users?filter=id%20eq%20"0b1d561c-21ff-4092-beab-8154b17f82f2"' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json"
+```
+
+Example response:
+
+```json
+{
+ "schemas": [
+ "urn:ietf:params:scim:api:messages:2.0:ListResponse"
+ ],
+ "totalResults": 1,
+ "itemsPerPage": 20,
+ "startIndex": 1,
+ "Resources": [
+ {
+ "schemas": [
+ "urn:ietf:params:scim:schemas:core:2.0:User"
+ ],
+ "id": "0b1d561c-21ff-4092-beab-8154b17f82f2",
+ "active": true,
+ "name.formatted": "Test User",
+ "userName": "username",
+ "meta": { "resourceType":"User" },
+ "emails": [
+ {
+ "type": "work",
+ "value": "name@example.com",
+ "primary": true
+ }
+ ]
+ }
+ ]
+}
+```
+
+## Get a single SAML user
+
+```text
+GET /api/scim/v2/groups/:group_path/Users/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | string | yes | External UID of the user. |
+| `group_path` | string | yes | Full path to the group. |
+
+Example request:
+
+```sh
+curl 'https://example.gitlab.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json"
+```
+
+Example response:
+
+```json
+{
+ "schemas": [
+ "urn:ietf:params:scim:schemas:core:2.0:User"
+ ],
+ "id": "0b1d561c-21ff-4092-beab-8154b17f82f2",
+ "active": true,
+ "name.formatted": "Test User",
+ "userName": "username",
+ "meta": { "resourceType":"User" },
+ "emails": [
+ {
+ "type": "work",
+ "value": "name@example.com",
+ "primary": true
+ }
+ ]
+}
+```
+
+## Create a SAML user
+
+```text
+POST /api/scim/v2/groups/:group_path/Users/
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|:---------------|:----------|:----|:--------------------------|
+| `externalId` | string | yes | External UID of the user. |
+| `userName` | string | yes | Username of the user. |
+| `emails` | JSON string | yes | Work email. |
+| `name` | JSON string | yes | Name of the user. |
+| `meta` | string | no | Resource type (`User'). |
+
+Example request:
+
+```sh
+curl --verbose --request POST 'https://example.gitlab.com/api/scim/v2/groups/test_group/Users' --data '{"externalId":"test_uid","active":null,"userName":"username","emails":[{"primary":true,"type":"work","value":"name@example.com"}],"name":{"formatted":"Test User","familyName":"User","givenName":"Test"},"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"meta":{"resourceType":"User"}}' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json"
+```
+
+Example response:
+
+```json
+{
+ "schemas": [
+ "urn:ietf:params:scim:schemas:core:2.0:User"
+ ],
+ "id": "0b1d561c-21ff-4092-beab-8154b17f82f2",
+ "active": true,
+ "name.formatted": "Test User",
+ "userName": "username",
+ "meta": { "resourceType":"User" },
+ "emails": [
+ {
+ "type": "work",
+ "value": "name@example.com",
+ "primary": true
+ }
+ ]
+}
+```
+
+Returns a `201` status code if successful.
+
+## Update a single SAML user
+
+Fields that can be updated are:
+
+| SCIM/IdP field | GitLab field |
+|:----------|:--------|
+| id/externalId | extern_uid |
+| name.formatted | name |
+| emails\[type eq "work"\].value | email |
+| active | Identity removal if `active = false` |
+| userName | username |
+
+```text
+PATCH /api/scim/v2/groups/:group_path/Users/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | string | yes | External UID of the user. |
+| `group_path` | string | yes | Full path to the group. |
+| `Operations` | JSON string | yes | An [operations](#available-operations) expression. |
+
+Example request:
+
+```sh
+curl --verbose --request PATCH 'https://example.gitlab.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2' --data '{ "Operations": [{"op":"Add","path":"name.formatted","value":"New Name"}] }' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json"
+```
+
+Returns an empty response with a `204` status code if successful.
+
+## Remove a single SAML user
+
+Removes the user's SSO identity and group membership.
+
+```text
+DELETE /api/scim/v2/groups/:group_path/Users/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|:----------|:--------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | string | yes | External UID of the user. |
+| `group_path` | string | yes | Full path to the group. |
+
+Example request:
+
+```sh
+curl --verbose --request DELETE 'https://example.gitlab.com/api/scim/v2/groups/test_group/Users/f0b1d561c-21ff-4092-beab-8154b17f82f2' --header "Authorization: Bearer <your_scim_token>" --header "Content-Type: application/scim+json"
+```
+
+Returns an empty response with a `204` status code if successful.
+
+## Available filters
+
+They match an expression as specified in [the RFC7644 filtering section](https://tools.ietf.org/html/rfc7644#section-3.4.2.2).
+
+| Filter | Description |
+| ----- | ----------- |
+| `eq` | The attribute matches exactly the specified value. |
+
+Example:
+
+```
+id eq a-b-c-d
+```
+
+## Available operations
+
+They perform an operation as specified in [the RFC7644 update section](https://tools.ietf.org/html/rfc7644#section-3.5.2).
+
+| Operator | Description |
+| ----- | ----------- |
+| `Replace` | The attribute's value is updated. |
+| `Add` | The attribute has a new value. |
+
+Example:
+
+```json
+{ "op": "Add", "path": "name.formatted", "value": "New Name" }
+```
diff --git a/doc/api/search.md b/doc/api/search.md
index 6ee3d32d8bc..c2dd72479c1 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -556,6 +556,7 @@ GET /projects/:id/search
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `scope` | string | yes | The scope to search in |
| `search` | string | yes | The search query |
+| `ref` | string | no | The name of a repository branch or tag to search on. The project's default branch is used by default. This is only applicable for scopes: commits, blobs, and wiki_blobs. |
Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users.
@@ -850,7 +851,7 @@ Blobs searches are performed on both filenames and contents. Search results:
times in the content.
```bash
-curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/6/search?scope=blobs&search=installation&ref=feature
```
Example response:
@@ -863,7 +864,7 @@ Example response:
"data": "```\n\n## Installation\n\nQuick start using the [pre-built",
"filename": "README.md",
"id": null,
- "ref": "master",
+ "ref": "feature",
"startline": 46,
"project_id": 6
}
diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md
new file mode 100644
index 00000000000..87f77613ad3
--- /dev/null
+++ b/doc/api/vulnerabilities.md
@@ -0,0 +1,113 @@
+# Vulnerabilities API
+
+Every API call to vulnerabilities must be authenticated.
+
+If a user is not a member of a project and the project is private, a `GET`
+request on that project will result in a `404` status code.
+
+CAUTION: **Caution:**
+This API is in an alpha stage and considered unstable.
+The response payload may be subject to change or breakage
+across GitLab releases.
+
+## Vulnerabilities pagination
+
+By default, `GET` requests return 20 results at a time because the API results
+are paginated.
+
+Read more on [pagination](README.md#pagination).
+
+## List project vulnerabilities
+
+List all of a project's vulnerabilities.
+
+```
+GET /projects/:id/vulnerabilities
+GET /projects/:id/vulnerabilities?report_type=sast
+GET /projects/:id/vulnerabilities?report_type=container_scanning
+GET /projects/:id/vulnerabilities?report_type=sast,dast
+GET /projects/:id/vulnerabilities?scope=all
+GET /projects/:id/vulnerabilities?scope=dismissed
+GET /projects/:id/vulnerabilities?severity=high
+GET /projects/:id/vulnerabilities?confidence=unknown,experimental
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `report_type` | Array[string] | no | Returns vulnerabilities belonging to specified report type. Valid values: `sast`, `dast`, `dependency_scanning`, or `container_scanning`. |
+| `scope` | string | no | Returns vulnerabilities for the given scope: `all` or `dismissed`. Defaults to `dismissed` |
+| `severity` | Array[string] | no | Returns vulnerabilities belonging to specified severity level: `undefined`, `info`, `unknown`, `low`, `medium`, `high`, or `critical`. Defaults to all' |
+| `confidence` | Array[string] | no | Returns vulnerabilities belonging to specified confidence level: `undefined`, `ignore`, `unknown`, `experimental`, `low`, `medium`, `high`, or `confirmed`. Defaults to all |
+
+```bash
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/vulnerabilities
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": null,
+ "report_type": "dependency_scanning",
+ "name": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
+ "severity": "unknown",
+ "confidence": "undefined",
+ "scanner": {
+ "external_id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "identifiers": [
+ {
+ "external_type": "gemnasium",
+ "external_id": "9952e574-7b5b-46fa-a270-aeb694198a98",
+ "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98",
+ "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories"
+ },
+ {
+ "external_type": "cve",
+ "external_id": "CVE-2017-11429",
+ "name": "CVE-2017-11429",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429"
+ }
+ ],
+ "project_fingerprint": "fa6f5b6c5d240b834ac5e901dc69f9484cef89ec",
+ "create_vulnerability_feedback_issue_path": "/tests/yarn-remediation-test/vulnerability_feedback",
+ "create_vulnerability_feedback_merge_request_path": "/tests/yarn-remediation-test/vulnerability_feedback",
+ "create_vulnerability_feedback_dismissal_path": "/tests/yarn-remediation-test/vulnerability_feedback",
+ "project": {
+ "id": 31,
+ "name": "yarn-remediation-test",
+ "full_path": "/tests/yarn-remediation-test",
+ "full_name": "tests / yarn-remediation-test"
+ },
+ "dismissal_feedback": null,
+ "issue_feedback": null,
+ "merge_request_feedback": null,
+ "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
+ "links": [
+ {
+ "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279"
+ },
+ {
+ "url": "https://www.kb.cert.org/vuls/id/475445"
+ },
+ {
+ "url": "https://github.com/Clever/saml2/issues/127"
+ }
+ ],
+ "location": {
+ "file": "yarn.lock",
+ "dependency": {
+ "package": {
+ "name": "saml2-js"
+ },
+ "version": "1.5.0"
+ }
+ },
+ "solution": "Upgrade to fixed version.\r\n",
+ "blob_path": "/tests/yarn-remediation-test/blob/cc6c4a0778460455ae5d16ca7025ca9ca1ca75ac/yarn.lock"
+ }
+]
+```
diff --git a/doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.png b/doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.png
deleted file mode 100644
index 3f2059504f5..00000000000
--- a/doc/ci/ci_cd_for_external_repos/img/github_omniauth_list.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index d5e6fbe8113..5a14ac17aec 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -268,7 +268,7 @@ For the value of:
which receives the value of the branch name.
- `environment:url`, we want a specific and distinct URL for each branch. `$CI_COMMIT_REF_NAME`
may contain a `/` or other characters that would be invalid in a domain name or URL,
- so we use `$CI_ENVIRONMENT_SLUG` to get a "clean" or "safe" URL.
+ so we use `$CI_ENVIRONMENT_SLUG` to guarantee that we get a valid URL.
For example, given a `$CI_COMMIT_REF_NAME` of `100-Do-The-Thing`, the URL will be something
like `https://100-do-the-4f99a2.example.com`. Again, the way you set up
@@ -351,7 +351,7 @@ deploy_prod:
```
A more realistic example would also include copying files to a location where a
-webserver (for example, NGINX) could then acess and serve them.
+webserver (for example, NGINX) could then access and serve them.
The example below will copy the `public` directory to `/srv/nginx/$CI_COMMIT_REF_SLUG/public`:
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 6b9f8181d1d..340a41c196b 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -18,30 +18,30 @@ Examples are available in several forms. As a collection of:
The following table lists examples for different use cases:
-| Use case | Resource |
-|:-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------|
-| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](browser_performance.md). |
-| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). |
-| Code quality analysis | [Analyze your project's Code Quality](code_quality.md). **[STARTER]** |
-| Container scanning | [Container Scanning with GitLab CI/CD](container_scanning.md). |
-| Dependency scanning | [Dependency Scanning with GitLab CI/CD](dependency_scanning.md). **[ULTIMATE]** |
-| Deployment with `dpl` | [Using `dpl` as deployment tool](deployment/README.md). |
-| Dynamic application<br>security testing (DAST) | [Dynamic Application Security Testing with GitLab CI/CD](dast.md) **[ULTIMATE]** |
-| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). |
-| Game development | [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md). |
-| GitLab Pages | See the [GitLab Pages](../../user/project/pages/index.md) documentation for a complete example. |
-| Java | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md). |
-| JUnit | [JUnit test reports](../junit_test_reports.md). |
-| License management | [Dependencies license management with GitLab CI/CD](license_management.md) **[ULTIMATE]** |
-| Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md). |
-| PHP | [Testing PHP projects](php.md). |
-| PHP | [Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD](deployment/composer-npm-deploy.md). |
-| PHP | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). |
-| Python | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). |
-| Ruby | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). |
-| Scala | [Test and deploy a Scala application to Heroku](test-scala-application.md). |
-| Static application<br>security testing (SAST) | [Static Application Security Testing with GitLab CI/CD](sast.md) **[ULTIMATE]** |
-| Testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). |
+| Use case | Resource |
+|:-----------------------------------------------|:------------------------------------------------------------------------------------------------------------------------|
+| Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](browser_performance.md). |
+| Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). |
+| Code quality analysis | [Analyze your project's Code Quality](code_quality.md). **[STARTER]** |
+| Container scanning | [Container Scanning with GitLab CI/CD](../../user/application_security/container_scanning/index.md). **[ULTIMATE]** |
+| Dependency scanning | [Dependency Scanning with GitLab CI/CD](../../user/application_security/dependency_scanning/index.md). **[ULTIMATE]** |
+| Deployment with `dpl` | [Using `dpl` as deployment tool](deployment/README.md). |
+| Dynamic application<br>security testing (DAST) | [Dynamic Application Security Testing with GitLab CI/CD](../../user/application_security/dast/index.md). **[ULTIMATE]** |
+| Elixir | [Testing a Phoenix application with GitLab CI/CD](test_phoenix_app_with_gitlab_ci_cd/index.md). |
+| Game development | [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md). |
+| GitLab Pages | See the [GitLab Pages](../../user/project/pages/index.md) documentation for a complete example. |
+| Java | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md). |
+| JUnit | [JUnit test reports](../junit_test_reports.md). |
+| License management | [Dependencies license management with GitLab CI/CD](../../user/application_security/license_management/index.md). **[ULTIMATE]** |
+| Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md). |
+| PHP | [Testing PHP projects](php.md). |
+| PHP | [Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD](deployment/composer-npm-deploy.md). |
+| PHP | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). |
+| Python | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). |
+| Ruby | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). |
+| Scala | [Test and deploy a Scala application to Heroku](test-scala-application.md). |
+| Static application<br>security testing (SAST) | [Static Application Security Testing with GitLab CI/CD](../../user/application_security/sast/index.md). **[ULTIMATE]** |
+| Testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). |
### Contributing examples
diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
index cf281605f5e..c622dd86828 100644
--- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
+++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md
@@ -4,6 +4,7 @@ author_gitlab: DylanGriffith
level: intermediate
article_type: tutorial
date: 2018-06-07
+last_updated: 2019-04-08
description: "Continuous Deployment of a Spring Boot application to Cloud Foundry with GitLab CI/CD"
---
@@ -77,7 +78,10 @@ image: java:8
stages:
- build
- deploy
-
+
+before_script:
+ - chmod +x mvnw
+
build:
stage: build
script: ./mvnw package
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png b/doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png
deleted file mode 100644
index bc188f83fb1..00000000000
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index 99a4316ab0d..47d20a4e1c1 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -9,7 +9,12 @@ You can checkout the [example source](https://gitlab.com/ayufan/python-getting-s
This is what the `.gitlab-ci.yml` file looks like for this project:
```yaml
+stages:
+ - test
+ - deploy
+
test:
+ stage: test
script:
# this configures Django application to use attached postgres database that is run on `postgres` host
- export DATABASE_URL=postgres://postgres:@postgres:5432/python-test-app
@@ -19,7 +24,7 @@ test:
- python manage.py test
staging:
- type: deploy
+ stage: deploy
script:
- apt-get update -qy
- apt-get install -y ruby-dev
@@ -29,7 +34,7 @@ staging:
- master
production:
- type: deploy
+ stage: deploy
script:
- apt-get update -qy
- apt-get install -y ruby-dev
diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/img/job-succeeded.png b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/img/job-succeeded.png
deleted file mode 100644
index 77b05f55f88..00000000000
--- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/img/job-succeeded.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index 14ea648c00b..0045fa2fb1f 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -156,7 +156,7 @@ Once you're happy with your implementation:
![GitLab workflow example](img/gitlab_workflow_example_11_9.png)
-GitLab CI/CD is capable of a doing a lot more, but this workflow
+GitLab CI/CD is capable of doing a lot more, but this workflow
exemplifies GitLab's ability to track the entire process,
without the need of any external tool to deliver your software.
And, most usefully, you can visualize all the steps through
diff --git a/doc/ci/merge_request_pipelines/img/pipeline_detail.png b/doc/ci/merge_request_pipelines/img/pipeline_detail.png
deleted file mode 100644
index 90e7c449a66..00000000000
--- a/doc/ci/merge_request_pipelines/img/pipeline_detail.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 3c26a38e3de..4c22026de3b 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -193,5 +193,5 @@ By using pipelines for merge requests, GitLab exposes additional predefined vari
Those variables contain information of the associated merge request, so that it's useful
to integrate your job with [GitLab Merge Request API](../../api/merge_requests.md).
-You can find the list of avilable variables in [the reference sheet](../variables/predefined_variables.md).
+You can find the list of available variables in [the reference sheet](../variables/predefined_variables.md).
The variable names begin with the `CI_MERGE_REQUEST_` prefix.
diff --git a/doc/ci/runners/shared_to_specific_admin.png b/doc/ci/runners/shared_to_specific_admin.png
deleted file mode 100644
index 8f4010a5849..00000000000
--- a/doc/ci/runners/shared_to_specific_admin.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index 2902c30c7c0..5fa378fc4c2 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -17,8 +17,8 @@ services:
variables:
# Configure mysql environment variables (https://hub.docker.com/_/mysql/)
- MYSQL_DATABASE: el_duderino
- MYSQL_ROOT_PASSWORD: mysql_strong_password
+ MYSQL_DATABASE: "<your_mysql_database>"
+ MYSQL_ROOT_PASSWORD: "<your_mysql_password>"
```
And then configure your application to use the database, for example:
@@ -26,18 +26,18 @@ And then configure your application to use the database, for example:
```yaml
Host: mysql
User: root
-Password: mysql_strong_password
-Database: el_duderino
+Password: <your_mysql_password>
+Database: <your_mysql_database>
```
If you are wondering why we used `mysql` for the `Host`, read more at
[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-mysql].
+You can also use any other docker image available on [Docker Hub](https://hub.docker.com/_/mysql/).
For example, to use MySQL 5.5 the service becomes `mysql:5.5`.
The `mysql` image can accept some environment variables. For more details
-check the documentation on [Docker Hub][hub-mysql].
+check the documentation on [Docker Hub](https://hub.docker.com/_/mysql/).
## Use MySQL with the Shell executor
@@ -74,13 +74,13 @@ mysql> CREATE USER 'runner'@'localhost' IDENTIFIED BY '$password';
Create the database:
```bash
-mysql> CREATE DATABASE IF NOT EXISTS `el_duderino` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
+mysql> CREATE DATABASE IF NOT EXISTS `<your_mysql_database>` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
```
Grant the necessary permissions on the database:
```bash
-mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `el_duderino`.* TO 'runner'@'localhost';
+mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `<your_mysql_database>`.* TO 'runner'@'localhost';
```
If all went well you can now quit the database session:
@@ -93,7 +93,7 @@ Now, try to connect to the newly created database to check that everything is
in place:
```bash
-mysql -u runner -p -D el_duderino
+mysql -u runner -p -D <your_mysql_database>
```
As a final step, configure your application to use the database, for example:
@@ -102,17 +102,14 @@ As a final step, configure your application to use the database, for example:
Host: localhost
User: runner
Password: $password
-Database: el_duderino
+Database: <your_mysql_database>
```
## Example project
-We have set up an [Example MySQL Project][mysql-example-repo] for your
+We have set up an [Example MySQL Project](https://gitlab.com/gitlab-examples/mysql) for your
convenience that runs on [GitLab.com](https://gitlab.com) using our publicly
available [shared runners](../runners/README.md).
-Want to hack on it? Simply fork it, commit and push your changes. Within a few
+Want to hack on it? Simply fork it, commit and push your changes. Within a few
moments the changes will be picked by a public runner and the job will begin.
-
-[hub-mysql]: https://hub.docker.com/_/mysql/
-[mysql-example-repo]: https://gitlab.com/gitlab-examples/mysql
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index dca2d953286..31ff56e06f8 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -337,6 +337,7 @@ In addition, `only` and `except` allow the use of special keywords:
| `triggers` | For pipelines created using a trigger token. |
| `web` | For pipelines created using **Run pipeline** button in GitLab UI (under your project's **Pipelines**). |
| `merge_requests` | When a merge request is created or updated (See [pipelines for merge requests](../merge_request_pipelines/index.md)). |
+| `chats` | For jobs created using a [GitLab ChatOps](../chatops/README.md) command. |
In the example below, `job` will run only for refs that start with `issue-`,
whereas all branches will be skipped:
@@ -1446,7 +1447,7 @@ be automatically shown in merge requests.
> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-The `sast` report collects [SAST vulnerabilities](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html)
+The `sast` report collects [SAST vulnerabilities](https://docs.gitlab.com/ee/user/application_security/sast/index.html)
as artifacts.
The collected SAST report will be uploaded to GitLab as an artifact and will
@@ -1509,6 +1510,8 @@ be automatically shown in merge requests.
##### `artifacts:reports:metrics` **[PREMIUM]**
+> Introduced in GitLab 11.10.
+
The `metrics` report collects [Metrics](../../ci/metrics_reports.md)
as artifacts.
@@ -1764,9 +1767,6 @@ TIP: **Tip:**
Use merging to customize and override included CI/CD configurations with local
definitions.
-Recursive includes are not supported. Your external files should not use the
-`include` keyword as it will be ignored.
-
NOTE: **Note:**
Using YAML aliases across different YAML files sourced by `include` is not
supported. You must only refer to aliases in the same file. Instead
diff --git a/doc/development/README.md b/doc/development/README.md
index 83a1145c020..2ff38d68a47 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -44,6 +44,7 @@ description: 'Learn how to contribute to GitLab.'
- [`Gemfile` guidelines](gemfile.md)
- [Pry debugging](pry_debugging.md)
- [Sidekiq debugging](sidekiq_debugging.md)
+- [Accessing session data](session.md)
- [Gotchas](gotchas.md) to avoid
- [Avoid modules with instance variables](module_with_instance_variables.md) if possible
- [How to dump production data to staging](db_dump.md)
@@ -136,3 +137,8 @@ description: 'Learn how to contribute to GitLab.'
## Go guides
- [Go Guidelines](go_guide/index.md)
+
+## Other GitLab Development Kit (GDK) guides
+
+- [Run full Auto DevOps cycle in a GDK instance](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/auto_devops.md)
+- [Using GitLab Runner with GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/runner.md)
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 88c16a8db22..9a012f4299b 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -10,34 +10,147 @@ For information, see the [GitLab Release Process](https://gitlab.com/gitlab-org/
Both EE and CE require some add-on components called gitlab-shell and Gitaly. These components are available from the [gitlab-shell](https://gitlab.com/gitlab-org/gitlab-shell/tree/master) and [gitaly](https://gitlab.com/gitlab-org/gitaly/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical.
-## GitLab Omnibus Component by Component
+## Components
-This document is designed to be consumed by systems adminstrators and GitLab Support Engineers who want to understand more about the internals of GitLab and how they work together.
+A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs.
-When deployed, GitLab should be considered the amalgamation of the below processes. When troubleshooting or debugging, be as specific as possible as to which component you are referencing. That should increase clarity and reduce confusion.
+We also support deploying GitLab on Kubernetes using our [gitlab Helm chart](https://docs.gitlab.com/charts/).
-### GitLab Process Descriptions
+The GitLab web app uses MySQL or PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository.
-As of this writing, a fresh GitLab 11.3.0 install will show the following processes with `gitlab-ctl status`:
+When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving git objects.
+The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access.
+
+Gitaly executes git operations from gitlab-shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files).
+
+You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/).
+
+### Component diagram
+
+```mermaid
+graph TB
+
+ HTTP[HTTP/HTTPS] -- TCP 80, 443 --> NGINX[NGINX]
+ SSH -- TCP 22 --> GitLabShell[GitLab Shell]
+ SMTP[SMTP Gateway]
+ Geo[GitLab Geo Node] -- TCP 22, 80, 443 --> NGINX
+
+ GitLabShell --TCP 8080 -->Unicorn["Unicorn (GitLab Rails)"]
+ GitLabShell --> Gitaly
+ GitLabShell --> Redis
+ Unicorn --> PgBouncer[PgBouncer]
+ Unicorn --> Redis
+ Unicorn --> Gitaly
+ Redis --> Sidekiq
+ Sidekiq["Sidekiq (GitLab Rails, ES Indexer)"] --> PgBouncer
+ GitLabWorkhorse[GitLab Workhorse] --> Unicorn
+ GitLabWorkhorse --> Redis
+ GitLabWorkhorse --> Gitaly
+ Gitaly --> Redis
+ NGINX --> GitLabWorkhorse
+ NGINX -- TCP 8090 --> GitLabPages[GitLab Pages]
+ NGINX --> Grafana[Grafana]
+ Grafana -- TCP 9090 --> Prometheus[Prometheus]
+ Prometheus -- TCP 80, 443 --> Unicorn
+ RedisExporter[Redis Exporter] --> Redis
+ Prometheus -- TCP 9121 --> RedisExporter
+ PostgreSQLExporter[PostgreSQL Exporter] --> PostgreSQL
+ PgBouncerExporter[PgBouncer Exporter] --> PgBouncer
+ Prometheus -- TCP 9187 --> PostgreSQLExporter
+ Prometheus -- TCP 9100 --> NodeExporter[Node Exporter]
+ Prometheus -- TCP 9168 --> GitLabMonito[GitLab Monitor]
+ Prometheus -- TCP 9127 --> PgBouncerExporter
+ GitLabMonitor --> PostgreSQL
+ GitLabMonitor --> GitLabShell
+ GitLabMonitor --> Sidekiq
+ PgBouncer --> Consul
+ PostgreSQL --> Consul
+ PgBouncer --> PostgreSQL
+ NGINX --> Registry
+ Unicorn --> Registry
+ NGINX --> Mattermost
+ Mattermost --- Unicorn
+ Prometheus --> Alertmanager
+ Migrations --> PostgreSQL
+ Runner -- TCP 443 --> NGINX
+ Unicorn -- TCP 9200 --> ElasticSearch
+ Sidekiq -- TCP 9200 --> ElasticSearch
+ Sidekiq -- TCP 80, 443 --> Sentry
+ Unicorn -- TCP 80, 443 --> Sentry
+ Sidekiq -- UDP 6831 --> Jaeger
+ Unicorn -- UDP 6831 --> Jaeger
+ Gitaly -- UDP 6831 --> Jaeger
+ GitLabShell -- UDP 6831 --> Jaeger
+ GitLabWorkhorse -- UDP 6831 --> Jaeger
+ Alertmanager -- TCP 25 --> SMTP
+ Sidekiq -- TCP 25 --> SMTP
+ Unicorn -- TCP 25 --> SMTP
+ Unicorn -- TCP 369 --> LDAP
+ Sidekiq -- TCP 369 --> LDAP
+ Unicorn -- TCP 443 --> ObjectStorage["Object Storage"]
+ Sidekiq -- TCP 443 --> ObjectStorage
+ GitLabWorkhorse -- TCP 443 --> ObjectStorage
+ Registry -- TCP 443 --> ObjectStorage
+ Geo -- TCP 5432 --> PostgreSQL
```
-run: alertmanager: (pid 30829) 14207s; run: log: (pid 13906) 2432044s
-run: gitaly: (pid 30771) 14210s; run: log: (pid 13843) 2432046s
-run: gitlab-monitor: (pid 30788) 14209s; run: log: (pid 13868) 2432045s
-run: gitlab-workhorse: (pid 30758) 14210s; run: log: (pid 13855) 2432046s
-run: logrotate: (pid 30246) 3407s; run: log: (pid 13825) 2432047s
-run: nginx: (pid 30849) 14207s; run: log: (pid 13856) 2432046s
-run: node-exporter: (pid 30929) 14206s; run: log: (pid 13877) 2432045s
-run: postgres-exporter: (pid 30935) 14206s; run: log: (pid 13931) 2432044s
-run: postgresql: (pid 13133) 2432214s; run: log: (pid 13848) 2432046s
-run: prometheus: (pid 30807) 14209s; run: log: (pid 13884) 2432045s
-run: redis: (pid 30560) 14274s; run: log: (pid 13807) 2432047s
-run: redis-exporter: (pid 30946) 14205s; run: log: (pid 13869) 2432045s
-run: sidekiq: (pid 30953) 14205s; run: log: (pid 13810) 2432047s
-run: unicorn: (pid 30960) 14204s; run: log: (pid 13809) 2432047s
-```
-### Layers
+### Component legend
+
+* ✅ - Installed by default
+* ⚙ - Requires additional configuration, or GitLab Managed Apps
+* ⤓ - Manual installation required
+* ❌ - Not supported or no instructions available
+
+Component statuses are linked to configuration documentation for each component.
+
+### Component list
+
+| Component | Description | [Omnibus GitLab](https://docs.gitlab.com/omnibus/README.html) | [GitLab chart](https://docs.gitlab.com/charts/) | [Minikube Minimal](https://docs.gitlab.com/charts/development/minikube/#deploying-gitlab-with-minimal-settings) | [GitLab.com](https://gitlab.com) | CE/EE |
+| --------- | ----------- |:--------------------:|:------------------:|:-----:|:--------:|:--------:|
+| [NGINX](#nginx) | Routes requests to appropriate components, terminates SSL | [✅][nginx-omnibus] | [✅][nginx-charts] | [⚙][nginx-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| [Unicorn (GitLab Rails)](#unicorn) | Handles requests for the web interface and API | [✅][unicorn-omnibus] | [✅][unicorn-charts] | [✅][unicorn-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#unicorn) | CE & EE |
+| [Sidekiq](#sidekiq) | Background jobs processor | [✅][sidekiq-omnibus] | [✅][sidekiq-charts] | [✅](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#sidekiq) | CE & EE |
+| [Gitaly](#gitaly) | Git RPC service for handling all git calls made by GitLab | [✅][gitaly-omnibus] | [✅][gitaly-charts] | [✅][gitaly-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| [GitLab Workhorse](#gitlab-workhorse) | Smart reverse proxy, handles large HTTP requests | [✅][workhorse-omnibus] | [✅][workhorse-charts] | [✅][workhorse-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| [GitLab Shell](#gitlab-shell) | Handles `git` over SSH sessions | [✅][shell-omnibus] | [✅][shell-charts] | [✅][shell-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| [GitLab Pages](#gitlab-pages) | Hosts static websites | [⚙][pages-omnibus] | [❌][pages-charts] | [❌][pages-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#gitlab-pages) | CE & EE |
+| [Registry](#registry) | Container registry, allows pushing and pulling of images | [⚙][registry-omnibus] | [✅][registry-charts] | [✅][registry-charts] | [✅](https://docs.gitlab.com/ee/user/project/container_registry.html#build-and-push-images) | CE & EE |
+| [Redis](#redis) | Caching service | [✅][redis-omnibus] | [✅][redis-omnibus] | [✅][redis-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| [PostgreSQL](#postgresql) | Database | [✅][postgres-omnibus] | [✅][postgres-charts] | [✅][postgres-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#postgresql) | CE & EE |
+| [PgBouncer](#pgbouncer) | Database connection pooling, failover | [⚙][pgbouncer-omnibus] | [❌][pgbouncer-charts] | [❌][pgbouncer-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#database-architecture) | EE Only |
+| [Consul](#consul) | Database node discovery, failover | [⚙][consul-omnibus] | [❌][consul-charts] | [❌][consul-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#consul) | EE Only |
+| [GitLab self-monitoring: Prometheus](#prometheus) | Time-series database, metrics collection, and query service | [✅][prometheus-omnibus] | [✅][prometheus-charts] | [⚙][prometheus-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#prometheus) | CE & EE |
+| [GitLab self-monitoring: Alertmanager](#alertmanager) | Deduplicates, groups, and routes alerts from Prometheus | [✅][alertmanager-omnibus] | [✅][alertmanager-charts] | [⚙][alertmanager-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| [GitLab self-monitoring: Grafana](#grafana) | Metrics dashboard | [⚙][grafana-omnibus] | [⤓][grafana-charts] | [⤓][grafana-charts] | [✅](https://dashboards.gitlab.com/d/RZmbBr7mk/gitlab-triage?refresh=30s) | CE & EE |
+| [GitLab self-monitoring: Sentry](#sentry) | Track errors generated by the GitLab instance | [⤓][sentry-omnibus] | [❌][sentry-charts] | [❌][sentry-charts] | [✅](https://about.gitlab.com/handbook/support/workflows/services/gitlab_com/500_errors.html#searching-sentry) | CE & EE |
+| [GitLab self-monitoring: Jaeger](#jaeger) | View traces generated by the GitLab instance | [❌][jaeger-omnibus] | [❌][jaeger-charts] | [❌][jaeger-charts] | [❌](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4104) | CE & EE |
+| [Redis Exporter](#redis-exporter) | Prometheus endpoint with Redis metrics | [✅][redis-exporter-omnibus] | [✅][redis-exporter-charts] | [✅][redis-exporter-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| [Postgres Exporter](#postgres-exporter) | Prometheus endpoint with PostgreSQL metrics | [✅][postgres-exporter-omnibus] | [✅][postgres-exporter-charts] | [✅][postgres-exporter-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| [PgBouncer Exporter](#pgbouncer-exporter) | Prometheus endpoint with PgBouncer metrics | [⚙][pgbouncer-exporter-omnibus] | [❌][pgbouncer-exporter-charts] | [❌][pgbouncer-exporter-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| [GitLab Monitor](#gitlab-monitor) | Generates a variety of GitLab metrics | [✅][gitlab-monitor-omnibus] | [❌][gitab-monitor-charts] | [❌][gitab-monitor-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| [Node Exporter](#node-exporter) | Prometheus endpoint with system metrics | [✅][node-exporter-omnibus] | [❌][node-exporter-charts] | [❌][node-exporter-charts] | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| [Mattermost](#mattermost) | Open-source Slack alternative | [⚙][mattermost-omnibus] | [⤓][mattermost-charts] | [⤓][mattermost-charts] | [⤓](https://docs.gitlab.com/ee/user/project/integrations/mattermost_slash_commands.html#manual-configuration), [⤓](https://docs.gitlab.com/ee/user/project/integrations/mattermost.html) | CE & EE |
+| [Minio](#minio) | Object storage service | [⤓][minio-omnibus] | [✅][minio-charts] | [✅][minio-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#storage-architecture) | CE & EE |
+| [Runner](#gitlab-runner) | Executes GitLab CI jobs | [⤓][runner-omnibus] | [✅][runner-charts] | [⚙][runner-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#shared-runners) | CE & EE |
+| [Database Migrations](#database-migrations) | Database migrations | [✅][database-migrations-omnibus] | [✅]() | [✅][database-migrations-charts] | [✅][database-migrations-charts] | CE & EE |
+| [Certificate Management](#certificate-management) | TLS Settings, Let's Encrypt | [✅][certificate-management-omnibus] | [✅][certificate-management-charts] | [⚙][certificate-management-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#secrets-management) | CE & EE |
+| [GitLab Geo Node](#gitlab-geo) | Geographically distributed GitLab nodes | [⚙][geo-omnibus] | [❌][geo-charts] | [❌][geo-charts] | ✅ | EE Only |
+| [LDAP Authentication](#ldap-authentication) | Authenticate users against centralized LDAP directory | [⤓][ldap-omnibus] | [⤓][ldap-charts] | [⤓][ldap-charts] | [❌](https://about.gitlab.com/pricing/#gitlab-com) | CE & EE |
+| [Outbound email (SMTP)](#outbound-email) | Send email messages to users | [⤓][outbound-email-omnibus] | [⤓][outbound-email-charts] | [⤓][outbound-email-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#mail-configuration) | CE & EE |
+| [Inbound email (SMTP)](#inbound-email) | Receive messages to update issues | [⤓][inbound-email-omnibus] | [⤓][inbound-email-charts] | [⤓][inbound-email-charts] | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#mail-configuration) | CE & EE |
+| [ElasticSearch](#elasticsearch) | Improved search within GitLab | [⤓][elasticsearch-omnibus] | [⤓][elasticsearch-charts] | [⤓][elasticsearch-charts] | [❌](https://gitlab.com/groups/gitlab-org/-/epics/153) | EE Only |
+| [Sentry integration](#sentry) | Error tracking for deployed apps | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | CE & EE |
+| [Jaeger integration](#jaeger) | Distributed tracing for deployed apps | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | EE Only |
+| [Kubernetes cluster apps](#kubernetes-cluster-apps) | Deploy [Helm](https://docs.helm.sh/), [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), [Cert-Manager](https://docs.cert-manager.io/en/latest/), [Prometheus](https://prometheus.io/docs/introduction/overview/), a [Runner](https://docs.gitlab.com/runner/), [JupyterHub](http://jupyter.org/), [Knative](https://cloud.google.com/knative) to a cluster | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | CE & EE |
+
+### Component details
+
+This document is designed to be consumed by systems administrators and GitLab Support Engineers who want to understand more about the internals of GitLab and how they work together.
+
+When deployed, GitLab should be considered the amalgamation of the below processes. When troubleshooting or debugging, be as specific as possible as to which component you are referencing. That should increase clarity and reduce confusion.
+
+**Layers**
GitLab can be considered to have two layers from a process perspective:
@@ -46,129 +159,274 @@ GitLab can be considered to have two layers from a process perspective:
- **Processors**: These processes are responsible for actually performing operations and presenting the service.
- **Data**: These services store/expose structured data for the GitLab service.
-### alertmanager
+#### Alertmanager
-- Omnibus configuration options
+- [Project page](https://github.com/prometheus/alertmanager/blob/master/README.md)
+- Configuration: [Omnibus][alertmanager-omnibus], [Charts][alertmanager-charts]
- Layer: Monitoring
-[Alert manager](https://prometheus.io/docs/alerting/alertmanager/) is a tool provided by prometheus that _"handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integration such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts."_ You can read more in [issue gitlab-ce#45740](https://gitlab.com/gitlab-org/gitlab-ce/issues/45740) about what we will be alerting on.
+[Alert manager](https://prometheus.io/docs/alerting/alertmanager/) is a tool provided by Prometheus that _"handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integration such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts."_ You can read more in [issue gitlab-ce#45740](https://gitlab.com/gitlab-org/gitlab-ce/issues/45740) about what we will be alerting on.
+
+#### Certificate management
+
+- Project page: [Omnibus](https://github.com/certbot/certbot/blob/master/README.rst), [Charts](https://github.com/jetstack/cert-manager/blob/master/README.md)
+- Configuration: [Omnibus][certificate-management-omnibus], [Charts][certificate-management-charts]
+- Layer: Core Service (Processor)
+
+#### Consul
+
+- [Project page](https://github.com/hashicorp/consul/blob/master/README.md)
+- Configuration: [Omnibus][consul-omnibus], [Charts][consul-charts]
+- Layer: Core Service (Data)
+
+Consul is a tool for service discovery and configuration. Consul is distributed, highly available, and extremely scalable.
+
+#### Database migrations
+
+- Configuration: [Omnibus][registry-omnibus], [Charts][registry-charts]
+- Layer: Core Service (Data)
+
+#### Elasticsearch
+
+- [Project page](https://github.com/elastic/elasticsearch/blob/master/README.textile)
+- Configuration: [Omnibus][elasticsearch-omnibus], [Charts][elasticsearch-charts]
+- Layer: Core Service (Data)
+
+Elasticsearch is a distributed RESTful search engine built for the cloud.
-### gitaly
+#### Gitaly
-- [Omnibus configuration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration)
+- [Project page](https://gitlab.com/gitlab-org/gitaly/blob/master/README.md)
+- Configuration: [Omnibus][gitaly-omnibus], [Charts][gitaly-charts]
- Layer: Core Service (Data)
-Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab (Think GitLab.com or High Availability Deployments). As of 11.3.0, this service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly).
+Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab (think GitLab.com or High Availability Deployments). As of 11.3.0, this service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly).
+
+#### Gitlab Geo
+
+- Configuration: [Omnibus][geo-omnibus], [Charts][geo-charts]
+- Layer: Core Service (Processor)
-### gitlab-monitor
+#### Gitlab Monitor
-- Omnibus configuration options
+- [Project page](https://gitlab.com/gitlab-org/gitlab-monitor)
+- Configuration: [Omnibus][gitlab-monitor-omnibus], [Charts][gitlab-monitor-charts]
- Layer: Monitoring
-GitLab Monitor is a process designed in house that allows us to export metrics about GitLab application internals to prometheus. You can read more [in the project's readme](https://gitlab.com/gitlab-org/gitlab-monitor)
+GitLab Monitor is a process designed in house that allows us to export metrics about GitLab application internals to Prometheus. You can read more [in the project's readme](https://gitlab.com/gitlab-org/gitlab-monitor).
-### gitlab-workhorse
+#### Gitlab Pages
-- Omnibus configuration options
+- Configuration: [Omnibus][pages-omnibus], [Charts][pages-charts]
- Layer: Core Service (Processor)
-[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) is a program designed at GitLab to help alleviate pressure from unicorn. You can read more about the [historical reasons for developing](https://about.gitlab.com/2016/04/12/a-brief-history-of-gitlab-workhorse/). It's designed to act as a smart reverse proxy to help speed up GitLab as a whole.
+GitLab Pages is a feature that allows you to publish static websites directly from a repository in GitLab.
+
+You can use it either for personal or business websites, such as portfolios, documentation, manifestos, and business presentations. You can also attribute any license to your content.
+
+#### Gitlab Runner
+
+- [Project page](https://gitlab.com/gitlab-org/gitlab-runner/blob/master/README.md)
+- Configuration: [Omnibus][runner-omnibus], [Charts][runner-charts]
+- Layer: Core Service (Processor)
-### logrotate
+GitLab Runner runs tests and sends the results to GitLab.
+
+GitLab CI is the open-source continuous integration service included with GitLab that coordinates the testing. The old name of this project was GitLab CI Multi Runner but please use "GitLab Runner" (without CI) from now on.
+
+#### Gitlab Shell
+
+- [Project page](https://gitlab.com/gitlab-org/gitlab-shell/blob/master/README.md)
+- Configuration: [Omnibus][shell-omnibus], [Charts][shell-charts]
+- Layer: Core Service (Processor)
+
+[GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell) is a program designed at GitLab to handle ssh-based `git` sessions, and modifies the list of authorized keys. GitLab Shell is not a Unix shell nor a replacement for Bash or Zsh.
+
+#### Gitlab Workhorse
+
+- [Project page](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/README.md)
+- Configuration: [Omnibus][gitlab-workhorse-omnibus], [Charts][gitlab-workhorse-charts]
+- Layer: Core Service (Processor)
-- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate)
+[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) is a program designed at GitLab to help alleviate pressure from Unicorn. You can read more about the [historical reasons for developing](https://about.gitlab.com/2016/04/12/a-brief-history-of-gitlab-workhorse/). It's designed to act as a smart reverse proxy to help speed up GitLab as a whole.
+
+#### Grafana
+
+- [Project page](https://github.com/grafana/grafana/blob/master/README.md)
+- Configuration: [Omnibus][grafana-omnibus], [Charts][grafana-charts]
+- Layer: Monitoring
+
+Grafana is an open source, feature rich metrics dashboard and graph editor for Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
+
+#### Jaeger
+
+- [Project page](https://github.com/jaegertracing/jaeger/blob/master/README.md)
+- Configuration: [Omnibus][jaeger-omnibus], [Charts][jaeger-charts]
+- Layer: Monitoring
+
+Jaeger, inspired by Dapper and OpenZipkin, is a distributed tracing system. It can be used for monitoring microservices-based distributed systems.
+
+#### Logrotate
+
+- [Project page](https://github.com/logrotate/logrotate/blob/master/README.md)
+- Configuration: [Omnibus](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate)
- Layer: Core Service
-GitLab is comprised of a large number of services that all log. We started bundling our own logrotate as of 7.4 to make sure we were logging responsibly. This is just a packaged version of the common opensource offering.
+GitLab is comprised of a large number of services that all log. We started bundling our own logrotate as of 7.4 to make sure we were logging responsibly. This is just a packaged version of the common open source offering.
+
+#### Mattermost
+
+- [Project page](https://github.com/mattermost/mattermost-server/blob/master/README.md)
+- Configuration: [Omnibus][mattermost-omnibus], [Charts][mattermost-charts]
+- Layer: Core Service (Processor)
+
+Mattermost is an open source, private cloud, Slack-alternative from https://mattermost.com.
+
+#### MinIO
+
+- [Project page](https://github.com/minio/minio/blob/master/README.md)
+- Configuration: [Omnibus][minio-omnibus], [Charts][minio-charts]
+- Layer: Core Service (Data)
+
+MinIO is an object storage server released under Apache License v2.0. It is compatible with Amazon S3 cloud storage service. It is best suited for storing unstructured data such as photos, videos, log files, backups and container / VM images. Size of an object can range from a few KBs to a maximum of 5TB.
-### nginx
+#### NGINX
-- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/nginx.html)
+- Project page: [Omnibus](https://github.com/nginx/nginx), [Charts](https://github.com/kubernetes/ingress-nginx/blob/master/README.md)
+- Configuration: [Omnibus][nginx-omnibus], [Charts][nginx-charts]
- Layer: Core Service (Processor)
Nginx as an ingress port for all HTTP requests and routes them to the approriate sub-systems within GitLab. We are bundling an unmodified version of the popular open source webserver.
-### node-exporter
+#### Node Exporter
-- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/node_exporter.html)
+- [Project page](https://github.com/prometheus/node_exporter/blob/master/README.md)
+- Configuration: [Omnibus][node-exporter-omnibus], [Charts][node-exporter-charts]
- Layer: Monitoring
-[Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine. (Think CPU/Disk/Load) It's just a packaged version of the common open source offering from the Prometheus project.
+[Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine (think CPU/Disk/Load). It's just a packaged version of the common open source offering from the Prometheus project.
+
+#### PgBouncer
+
+- [Project page](https://github.com/pgbouncer/pgbouncer/blob/master/README.md)
+- Configuration: [Omnibus][pgbouncer-omnibus], [Charts][pgbouncer-charts]
+- Layer: Core Service (Data)
-### postgres-exporter
+Lightweight connection pooler for PostgreSQL.
-- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html)
+#### PgBouncer Exporter
+
+- [Project page](https://github.com/stanhu/pgbouncer_exporter/blob/master/README.md)
+- Configuration: [Omnibus][pgbouncer-exporter-omnibus], [Charts][pgbouncer-exporter-charts]
- Layer: Monitoring
-[Postgres-exporter](https://github.com/wrouesnel/postgres_exporter) is the community provided Prometheus exporter that will deliver data about Postgres to prometheus for use in Grafana Dashboards.
+Prometheus exporter for PgBouncer. Exports metrics at 9127/metrics.
-### postgresql
+#### Postgresql
-- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/database.html)
+- [Project page](https://github.com/postgres/postgres/blob/master/README)
+- Configuration: [Omnibus][postgres-omnibus], [Charts][postgres-charts]
- Layer: Core Service (Data)
GitLab packages the popular Database to provide storage for Application meta data and user information.
-### prometheus
+#### Postgres Exporter
-- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/)
+- [Project page](https://github.com/wrouesnel/postgres_exporter/blob/master/README.md)
+- Configuration: [Omnibus][postgres-exporter-omnibus], [Charts][postgres-exporter-charts]
+- Layer: Monitoring
+
+[Postgres-exporter](https://github.com/wrouesnel/postgres_exporter) is the community provided Prometheus exporter that will deliver data about Postgres to Prometheus for use in Grafana Dashboards.
+
+#### Prometheus
+
+- [Project page](https://github.com/prometheus/prometheus/blob/master/README.md)
+- Configuration: [Omnibus][prometheus-omnibus], [Charts][prometheus-charts]
- Layer: Monitoring
Prometheus is a time-series tool that helps GitLab administrators expose metrics about the individual processes used to provide GitLab the service.
-### redis
+#### Redis
-- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/redis.html)
+- [Project page](https://github.com/antirez/redis/blob/unstable/README.md)
+- Configuration: [Omnibus][redis-omnibus], [Charts][redis-charts]
- Layer: Core Service (Data)
Redis is packaged to provide a place to store:
- session data
- temporary cache information
-- background job queues.
+- background job queues
-### redis-exporter
+#### Redis Exporter
-- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/redis_exporter.html)
+- [Project page](https://github.com/oliver006/redis_exporter/blob/master/README.md)
+- Configuration: [Omnibus][redis-exporter-omnibus], [Charts][redis-exporter-charts]
- Layer: Monitoring
-[Redis Exporter](https://github.com/oliver006/redis_exporter) is designed to give specific metrics about the Redis process to Prometheus so that we can graph these metrics in Graphana.
+[Redis Exporter](https://github.com/oliver006/redis_exporter) is designed to give specific metrics about the Redis process to Prometheus so that we can graph these metrics in Grafana.
-### sidekiq
+#### Registry
-- Omnibus configuration options
+- [Project page](https://github.com/docker/distribution/blob/master/README.md)
+- Configuration: [Omnibus][registry-omnibus], [Charts][registry-charts]
+- Layer: Core Service (Processor)
+
+The registry is what users use to store their own Docker images. The bundled
+registry uses nginx as a load balancer and GitLab as an authentication manager.
+Whenever a client requests to pull or push an image from the registry, it will
+return a `401` response along with a header detailing where to get an
+authentication token, in this case the GitLab instance. The client will then
+request a pull or push auth token from GitLab and retry the original request
+to the registry. Learn more about [token authentication](https://docs.docker.com/registry/spec/auth/token/).
+
+An external registry can also be configured to use GitLab as an auth endpoint.
+
+#### Sentry
+
+- [Project page](https://github.com/getsentry/sentry/blob/master/README.rst)
+- Configuration: [Omnibus][sentry-omnibus], [Charts][sentry-charts]
+- Layer: Monitoring
+
+Sentry fundamentally is a service that helps you monitor and fix crashes in realtime. The server is in Python, but it contains a full API for sending events from any language, in any application.
+
+#### Sidekiq
+
+- [Project page](https://github.com/mperham/sidekiq/blob/master/README.md)
+- Configuration: [Omnibus][sidekiq-omnibus], [Charts][sidekiq-charts]
- Layer: Core Service (Processor)
Sidekiq is a Ruby background job processor that pulls jobs from the redis queue and processes them. Background jobs allow GitLab to provide a faster request/response cycle by moving work into the background.
-### unicorn
+#### Unicorn
-- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/unicorn.html)
+- [Project page](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/README.md)
+- Configuration: [Omnibus][unicorn-omnibus], [Charts][unicorn-charts]
- Layer: Core Service (Processor)
[Unicorn](https://bogomips.org/unicorn/) is a Ruby application server that is used to run the core Rails Application that provides the user facing features in GitLab. Often process output you will see this as `bundle` or `config.ru` depending on the GitLab version.
-### Additional Processes
+#### LDAP Authentication
-### GitLab Pages
+- Configuration: [Omnibus][ldap-omnibus], [Charts][ldap-charts]
+- Layer: Core Service (Processor)
-TODO
+#### Outbound Email
-### Mattermost
+- Configuration: [Omnibus][outbound-email-omnibus], [Charts][outbound-email-charts]
+- Layer: Core Service (Processor)
-TODO
+#### Inbound Email
-### Registry
+- Configuration: [Omnibus][inbound-email-omnibus], [Charts][inbound-email-charts]
+- Layer: Core Service (Processor)
-The registry is what users use to store their own Docker images. The bundled
-registry uses nginx as a load balancer and GitLab as an authentication manager.
-Whenever a client requests to pull or push an image from the registry, it will
-return a `401` response along with a header detailing where to get an
-authentication token, in this case the GitLab instance. The client will then
-request a pull or push auth token from GitLab and retry the original request
-to the registry. Learn more about [token authentication](https://docs.docker.com/registry/spec/auth/token/).
+#### Kubernetes Cluster Apps
-An external registry can also be configured to use GitLab as an auth endpoint.
+- Configuration: [Omnibus][managed-k8s-apps], [Charts][managed-k8s-apps]
+- Layer: Core Service (Processor)
+
+GitLab provides [GitLab Managed Apps](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications), a one-click install for various applications which can be added directly to your configured cluster. These applications are needed for Review Apps and deployments when using Auto DevOps. You can install them after you create a cluster.
## GitLab by Request Type
@@ -181,10 +439,10 @@ It's important to understand the distinction as some processes are used in both
### GitLab Web HTTP Request Cycle
-When making a request to an HTTP Endpoint (Think `/users/sign_in`) the request will take the following path through the GitLab Service:
+When making a request to an HTTP Endpoint (think `/users/sign_in`) the request will take the following path through the GitLab Service:
-- nginx - Acts as our first line reverse proxy
-- gitlab-workhorse - This determines if it needs to go to the Rails application or somewhere else to reduce load on unicorn.
+- nginx - Acts as our first line reverse proxy.
+- gitlab-workhorse - This determines if it needs to go to the Rails application or somewhere else to reduce load on Unicorn.
- unicorn - Since this is a web request, and it needs to access the application it will go to Unicorn.
- Postgres/Gitaly/Redis - Depending on the type of request, it may hit these services to store or retrieve data.
@@ -200,7 +458,7 @@ TODO
## System Layout
-When referring to `~git` in the pictures it means the home directory of the git user which is typically /home/git.
+When referring to `~git` in the pictures it means the home directory of the git user which is typically `/home/git`.
GitLab is primarily installed within the `/home/git` user home directory as `git` user. Within the home directory is where the gitlabhq server software resides as well as the repositories (though the repository location is configurable).
@@ -208,136 +466,6 @@ The bare repositories are located in `/home/git/repositories`. GitLab is a ruby
To serve repositories over SSH there's an add-on application called gitlab-shell which is installed in `/home/git/gitlab-shell`.
-### Components
-
-```mermaid
-graph TB
-
- HTTP[HTTP/HTTPS] -- TCP 80, 443 --> NGINX[NGINX]
- SSH -- TCP 22 --> GitLabShell[GitLab Shell]
- SMTP[SMTP Gateway]
- Geo[GitLab Geo Node] -- TCP 22, 80, 443 --> NGINX
-
- GitLabShell --TCP 8080 -->Unicorn["Unicorn (GitLab Rails)"]
- GitLabShell --> Gitaly
- GitLabShell --> Redis
- Unicorn --> PgBouncer[PgBouncer]
- Unicorn --> Redis
- Unicorn --> Gitaly
- Redis --> Sidekiq
- Sidekiq["Sidekiq (GitLab Rails, ES Indexer)"] --> PgBouncer
- GitLabWorkhorse[GitLab Workhorse] --> Unicorn
- GitLabWorkhorse --> Redis
- GitLabWorkhorse --> Gitaly
- Gitaly --> Redis
- NGINX --> GitLabWorkhorse
- NGINX -- TCP 8090 --> GitLabPages[GitLab Pages]
- NGINX --> Grafana[Grafana]
- Grafana -- TCP 9090 --> Prometheus[Prometheus]
- Prometheus -- TCP 80, 443 --> Unicorn
- RedisExporter[Redis Exporter] --> Redis
- Prometheus -- TCP 9121 --> RedisExporter
- PostgreSQLExporter[PostgreSQL Exporter] --> PostgreSQL
- PgBouncerExporter[PgBouncer Exporter] --> PgBouncer
- Prometheus -- TCP 9187 --> PostgreSQLExporter
- Prometheus -- TCP 9100 --> NodeExporter[Node Exporter]
- Prometheus -- TCP 9168 --> GitLabMonito[GitLab Monitor]
- Prometheus -- TCP 9127 --> PgBouncerExporter
- GitLabMonitor --> PostgreSQL
- GitLabMonitor --> GitLabShell
- GitLabMonitor --> Sidekiq
- PgBouncer --> Consul
- PostgreSQL --> Consul
- PgBouncer --> PostgreSQL
- NGINX --> Registry
- Unicorn --> Registry
- NGINX --> Mattermost
- Mattermost --- Unicorn
- Prometheus --> Alertmanager
- Migrations --> PostgreSQL
- Runner -- TCP 443 --> NGINX
- Unicorn -- TCP 9200 --> ElasticSearch
- Sidekiq -- TCP 9200 --> ElasticSearch
- Sidekiq -- TCP 80, 443 --> Sentry
- Unicorn -- TCP 80, 443 --> Sentry
- Sidekiq -- UDP 6831 --> Jaeger
- Unicorn -- UDP 6831 --> Jaeger
- Gitaly -- UDP 6831 --> Jaeger
- GitLabShell -- UDP 6831 --> Jaeger
- GitLabWorkhorse -- UDP 6831 --> Jaeger
- Alertmanager -- TCP 25 --> SMTP
- Sidekiq -- TCP 25 --> SMTP
- Unicorn -- TCP 25 --> SMTP
- Unicorn -- TCP 369 --> LDAP
- Sidekiq -- TCP 369 --> LDAP
- Unicorn -- TCP 443 --> ObjectStorage["Object Storage"]
- Sidekiq -- TCP 443 --> ObjectStorage
- GitLabWorkhorse -- TCP 443 --> ObjectStorage
- Registry -- TCP 443 --> ObjectStorage
- Geo -- TCP 5432 --> PostgreSQL
-
-```
-
----
-
-**Legend**:
-
-* ✅ - Automatically configured
-* ⚙ - Requires additional configuration
-* ⤓ - Additional software/service required
-* ❌ - Not available
-
-| Component | Description | [Omnibus GitLab](https://docs.gitlab.com/omnibus/README.html) | [GitLab chart](https://docs.gitlab.com/charts/) | [Minikube Minimal](https://docs.gitlab.com/charts/development/minikube/#deploying-gitlab-with-minimal-settings) | [GitLab.com](https://gitlab.com) | CE/EE |
-| --------- | ----------- |:--------------------:|:------------------:|:-----:|:--------:|:--------:|
-| NGINX | Routes requests to appropriate components, terminates SSL | [✅](https://docs.gitlab.com/omnibus/settings/nginx.html) | [✅](https://docs.gitlab.com/charts/charts/nginx/index.html) | [⚙](https://docs.gitlab.com/charts/charts/nginx/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
-| Unicorn (GitLab Rails) | Handles requests for the web interface and API | [✅](https://docs.gitlab.com/omnibus/settings/unicorn.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#unicorn) | CE & EE |
-| Sidekiq | Background jobs processor | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#sidekiq) | CE & EE |
-| Gitaly | Git RPC service for handling all git calls made by GitLab | [✅](https://docs.gitlab.com/ee/administration/gitaly/) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitaly/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitaly/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
-| GitLab Workhorse | Smart reverse proxy, handles large HTTP requests | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
-| GitLab Shell | Handles `git` over SSH sessions | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitlab-shell/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitlab-shell/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
-| GitLab Pages | Hosts static websites | [⚙](https://docs.gitlab.com/ee/administration/pages/) | [❌](https://gitlab.com/charts/gitlab/issues/37) | [❌](https://gitlab.com/charts/gitlab/issues/37) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#gitlab-pages) | CE & EE |
-| Registry | Container registry, allows pushing and pulling of images | [⚙](https://docs.gitlab.com/ee/administration/container_registry.html#container-registry-domain-configuration) | [✅](https://docs.gitlab.com/charts/charts/registry/index.html) | [✅](https://docs.gitlab.com/charts/charts/registry/index.html) | [✅](https://docs.gitlab.com/ee/user/project/container_registry.html#build-and-push-images) | CE & EE |
-| Redis | Caching service | [✅](https://docs.gitlab.com/omnibus/settings/redis.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
-| PostgreSQL | Database | [✅](https://docs.gitlab.com/omnibus/settings/database.html) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#postgresql) | CE & EE |
-| PgBouncer | Database connection pooling, failover | [⚙](https://docs.gitlab.com/ee/administration/high_availability/pgbouncer.html) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#database-architecture) | EE Only |
-| Consul | Database node discovery, failover | [⚙](https://docs.gitlab.com/ee/administration/high_availability/consul.html) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#consul) | EE Only |
-| Prometheus | Time-series database, metrics collection, and query service | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/) | [✅](https://github.com/helm/charts/tree/master/stable/prometheus) | [⚙](https://github.com/helm/charts/tree/master/stable/prometheus) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#prometheus) | CE & EE |
-| Prometheus Alertmanager | Deduplicates, groups, and routes alerts from Prometheus | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://github.com/helm/charts/tree/master/stable/prometheus) | [✅](https://github.com/helm/charts/tree/master/stable/prometheus) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
-| Grafana | Metrics dashboard | [⚙](https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html) | [⤓](https://github.com/helm/charts/tree/master/stable/grafana) | [⤓](https://github.com/helm/charts/tree/master/stable/grafana) | [✅](https://dashboards.gitlab.com/d/RZmbBr7mk/gitlab-triage?refresh=30s) | CE & EE |
-| Redis Exporter | Prometheus endpoint with Redis metrics | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/redis_exporter.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
-| PostgreSQL Exporter | Prometheus endpoint with PostgreSQL metrics | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
-| PgBouncer Exporter | Prometheus endpoint with PgBouncer metrics | [⚙](https://docs.gitlab.com/ee/administration/monitoring/prometheus/pgbouncer_exporter.html) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
-| GitLab Monitor | Generates a variety of GitLab metrics | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/gitlab_monitor_exporter.html) | [❌](https://gitlab.com/charts/gitlab/issues/319) | [❌](https://gitlab.com/charts/gitlab/issues/319) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
-| Mattermost | Open-source Slack alternative | [⚙](https://docs.gitlab.com/omnibus/gitlab-mattermost/) | [⤓](https://docs.mattermost.com/install/install-mmte-helm-gitlab-helm.html) | [⤓](https://docs.mattermost.com/install/install-mmte-helm-gitlab-helm.html) | [⤓](https://docs.gitlab.com/ee/user/project/integrations/mattermost_slash_commands.html#manual-configuration), [⤓](https://docs.gitlab.com/ee/user/project/integrations/mattermost.html) | CE & EE |
-| Minio | Object storage service | [⤓](https://min.io/download) | [✅](https://docs.gitlab.com/charts/charts/minio/index.html) | [✅](https://docs.gitlab.com/charts/charts/minio/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#storage-architecture) | CE & EE |
-| Runner | Executes GitLab CI jobs | [⤓](https://docs.gitlab.com/runner/) | [✅](https://docs.gitlab.com/runner/) | [⚙](https://docs.gitlab.com/runner/) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#shared-runners) | CE & EE |
-| DB Migrations | Database migrations | [✅](https://docs.gitlab.com/omnibus/settings/database.html#disabling-automatic-database-migration) | [✅](https://docs.gitlab.com/charts/charts/gitlab/migrations/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/migrations/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#database-architecture) | CE & EE |
-| Certificate Management | TLS Settings, Let's Encrypt | [✅](https://docs.gitlab.com/omnibus/settings/ssl.html) | [✅](https://docs.gitlab.com/charts/installation/tls.html) | [⚙](https://docs.gitlab.com/charts/installation/tls.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#secrets-management) | CE & EE |
-| GitLab Geo Node | Geographically distributed GitLab nodes | [⚙](https://docs.gitlab.com/ee/administration/geo/replication/index.html#setup-instructions) | [❌](https://gitlab.com/charts/gitlab/issues/8) | [❌](https://gitlab.com/charts/gitlab/issues/8) | ✅ | EE Only |
-| LDAP Authentication | Authenticate users against centralized LDAP directory | [⤓](https://docs.gitlab.com/ee/administration/auth/ldap.html) | [⤓](https://docs.gitlab.com/charts/charts/globals.html#ldap) | [⤓](https://docs.gitlab.com/charts/charts/globals.html#ldap) | [❌](https://about.gitlab.com/pricing/#gitlab-com) | CE & EE |
-| Outbound email (SMTP) | Send email messages to users | [⤓](https://docs.gitlab.com/omnibus/settings/smtp.html) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#outgoing-email-configuration) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#outgoing-email-configuration) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#mail-configuration) | CE & EE |
-| Inbound email (SMTP) | Receive messages to update issues | [⤓](https://docs.gitlab.com/ee/administration/incoming_email.html) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#incoming-email-configuration) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#incoming-email-configuration) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#mail-configuration) | CE & EE |
-| ElasticSearch | Improved search within GitLab | [⤓](https://docs.gitlab.com/ee/integration/elasticsearch.html) | [⤓](https://docs.gitlab.com/ee/integration/elasticsearch.html) | [⤓](https://docs.gitlab.com/ee/integration/elasticsearch.html) | [❌](https://gitlab.com/groups/gitlab-org/-/epics/153) | EE Only |
-| Sentry: GitLab instance | Track errors generated by the GitLab instance | [⤓](https://docs.gitlab.com/omnibus/settings/configuration.html#error-reporting-and-logging-with-sentry) | [❌](https://gitlab.com/charts/gitlab/issues/1319) | [❌](https://gitlab.com/charts/gitlab/issues/1319) | [✅](https://about.gitlab.com/handbook/support/workflows/services/gitlab_com/500_errors.html#searching-sentry) | CE & EE |
-| Jaeger: GitLab instance | View traces generated by the GitLab instance | [❌](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4104) | [❌](https://gitlab.com/charts/gitlab/issues/1320) | [❌](https://gitlab.com/charts/gitlab/issues/1320) | [❌](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4104) | CE & EE |
-| Sentry: deployed apps | Error tracking for deployed apps | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | CE & EE |
-| Jaeger: deployed apps | Distributed tracing for deployed apps | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | EE Only |
-| Kubernetes cluster apps | Deploy [Helm](https://docs.helm.sh/), [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), [Cert-Manager](https://docs.cert-manager.io/en/latest/), [Prometheus](https://prometheus.io/docs/introduction/overview/), a [Runner](https://docs.gitlab.com/runner/), [JupyterHub](http://jupyter.org/), [Knative](https://cloud.google.com/knative) to a cluster | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | CE & EE |
-
-A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs.
-
-We also support deploying GitLab on Kubernetes using our [gitlab Helm chart](https://docs.gitlab.com/charts/).
-
-The GitLab web app uses MySQL or PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository.
-
-When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving git objects.
-
-The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access.
-
-Gitaly executes git operations from gitlab-shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files).
-
-You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/).
-
### Installation Folder Summary
To summarize here's the [directory structure of the `git` user home directory](../install/structure.md).
@@ -464,3 +592,71 @@ Note: It is recommended to log into the `git` user using `sudo -i -u git` or `su
## GitLab.com
We've also detailed [our architecture of GitLab.com](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/) but this is probably over the top unless you have millions of users.
+
+[alertmanager-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template
+[alertmanager-charts]: https://github.com/helm/charts/tree/master/stable/prometheus
+[nginx-omnibus]: https://docs.gitlab.com/omnibus/settings/nginx.html
+[nginx-charts]: https://docs.gitlab.com/charts/charts/nginx/index.html
+[unicorn-omnibus]: https://docs.gitlab.com/omnibus/settings/unicorn.html
+[unicorn-charts]: https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html
+[sidekiq-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template
+[sidekiq-charts]: https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html
+[gitaly-omnibus]: https://docs.gitlab.com/ee/administration/gitaly/
+[gitaly-charts]: https://docs.gitlab.com/charts/charts/gitlab/gitaly/index.html
+[workhorse-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template
+[workhorse-charts]: https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html
+[shell-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template
+[shell-charts]: https://docs.gitlab.com/charts/charts/gitlab/gitlab-shell/index.html
+[pages-omnibus]: https://docs.gitlab.com/ee/administration/pages/
+[pages-charts]: https://gitlab.com/charts/gitlab/issues/37
+[registry-omnibus]: https://docs.gitlab.com/ee/administration/container_registry.html#container-registry-domain-configuration
+[registry-charts]: https://docs.gitlab.com/charts/charts/registry/index.html
+[redis-omnibus]: https://docs.gitlab.com/omnibus/settings/redis.html
+[redis-charts]: https://docs.gitlab.com/charts/charts/redis/index.html
+[postgres-omnibus]: https://docs.gitlab.com/omnibus/settings/database.html
+[postgres-charts]: https://github.com/helm/charts/tree/master/stable/postgresql
+[pgbouncer-omnibus]: https://docs.gitlab.com/ee/administration/high_availability/pgbouncer.html
+[pgbouncer-charts]: https://docs.gitlab.com/charts/installation/deployment.html#postgresql
+[consul-omnibus]: https://docs.gitlab.com/ee/administration/high_availability/consul.html
+[consul-charts]: https://docs.gitlab.com/charts/installation/deployment.html#postgresql
+[prometheus-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/
+[prometheus-charts]: https://github.com/helm/charts/tree/master/stable/prometheus
+[grafana-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html
+[grafana-charts]: https://github.com/helm/charts/tree/master/stable/grafana
+[sentry-omnibus]: https://docs.gitlab.com/omnibus/settings/configuration.html#error-reporting-and-logging-with-sentry
+[sentry-charts]: https://gitlab.com/charts/gitlab/issues/1319
+[jaeger-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4104
+[jaeger-charts]: https://gitlab.com/charts/gitlab/issues/1320
+[redis-exporter-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/redis_exporter.html
+[redis-exporter-charts]: https://docs.gitlab.com/charts/charts/redis/index.html
+[postgres-exporter-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html
+[postgres-exporter-charts]: https://github.com/helm/charts/tree/master/stable/postgresql
+[pgbouncer-exporter-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/pgbouncer_exporter.html
+[pgbouncer-exporter-charts]: https://docs.gitlab.com/charts/installation/deployment.html#postgresql
+[gitlab-monitor-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/gitlab_monitor_exporter.html
+[gitab-monitor-charts]: https://gitlab.com/charts/gitlab/issues/319
+[node-exporter-omnibus]: https://docs.gitlab.com/ee/administration/monitoring/prometheus/node_exporter.html
+[node-exporter-charts]: https://gitlab.com/charts/gitlab/issues/1332
+[mattermost-omnibus]: https://docs.gitlab.com/omnibus/gitlab-mattermost/
+[mattermost-charts]: https://docs.mattermost.com/install/install-mmte-helm-gitlab-helm.html
+[minio-omnibus]: https://min.io/download
+[minio-charts]: https://docs.gitlab.com/charts/charts/minio/index.html
+[runner-omnibus]: https://docs.gitlab.com/runner/
+[runner-charts]: https://docs.gitlab.com/runner/install/kubernetes.html
+[database-migrations-omnibus]: https://docs.gitlab.com/omnibus/settings/database.html#disabling-automatic-database-migration
+[database-migrations-charts]: https://docs.gitlab.com/charts/charts/gitlab/migrations/index.html
+[certificate-management-omnibus]: https://docs.gitlab.com/omnibus/settings/ssl.html
+[certificate-management-charts]: https://docs.gitlab.com/charts/installation/tls.html
+[geo-omnibus]: https://docs.gitlab.com/ee/administration/geo/replication/index.html#setup-instructions
+[geo-charts]: https://gitlab.com/charts/gitlab/issues/8
+[ldap-omnibus]: https://docs.gitlab.com/ee/administration/auth/ldap.html
+[ldap-charts]: https://docs.gitlab.com/charts/charts/globals.html#ldap
+[outbound-email-omnibus]: https://docs.gitlab.com/omnibus/settings/smtp.html
+[outbound-email-charts]: https://docs.gitlab.com/charts/installation/command-line-options.html#outgoing-email-configuration
+[inbound-email-omnibus]: https://docs.gitlab.com/ee/administration/incoming_email.html
+[inbound-email-charts]: https://docs.gitlab.com/charts/installation/command-line-options.html#incoming-email-configuration
+[elasticsearch-omnibus]: https://docs.gitlab.com/ee/integration/elasticsearch.html
+[elasticsearch-charts]: https://docs.gitlab.com/ee/integration/elasticsearch.html
+[sentry-integration]: https://docs.gitlab.com/ee/user/project/operations/error_tracking.html
+[jaeger-integration]: https://docs.gitlab.com/ee/user/project/operations/tracing.html
+[managed-k8s-apps]: https://docs.gitlab.com/ee/user/project/clusters/#installing-applications
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 52a4bb27817..4bf8401c0e8 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -52,6 +52,57 @@ Adhere to the [Documentation Style Guide](styleguide.md). If a style standard is
See the [Structure](styleguide.md#structure) section of the [Documentation Style Guide](styleguide.md).
+## Single codebase
+
+We currently maintain two sets of docs: one in the
+[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc) repo and
+one in [gitlab-ee](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc).
+They are similar, and most pages are identical, but they are different repositories.
+With the single codebase effort, we want to make those two sets identical, so when the
+time comes to have only one codebase, we'll be ready.
+
+Here are some links to get you up to speed with the current effort:
+
+- [CE/EE codebases blueprint](https://about.gitlab.com/handbook/engineering/infrastructure/blueprint/ce-ee-codebases/)
+- [CE/EE codebases merge design](https://about.gitlab.com/handbook/engineering/infrastructure/design/merge-ce-ee-codebases/)
+- [Single docs codebase epic](https://gitlab.com/groups/gitlab-org/-/epics/199)
+- [Issue board of related issues](https://gitlab.com/groups/gitlab-org/-/boards/981090?&label_name[]=Documentation&label_name[]=single%20codebase)
+- [Related merge requests](https://gitlab.com/groups/gitlab-org/-/merge_requests?scope=all&utf8=%E2%9C%93&state=all&label_name[]=Documentation&label_name[]=single%20codebase)
+- [Visualize the existing diffs](https://leipert-projects.gitlab.io/is-gitlab-pretty-yet/diff/?search=%5Edoc)
+
+### CE first
+
+After a given documentation path is aligned across CE and EE, all merge requests
+affecting that path must be submitted to CE, regardless of the content it has.
+This means that for EE-only features which are being added only to the EE codebase,
+you have to submit a separate merge request in CE that contains the docs.
+This might seem like a duplicate effort, but it's for the short term.
+A list of the already aligned docs can be found in
+[the epic description](https://gitlab.com/groups/gitlab-org/-/epics/199#ee-specific-lines-check).
+
+Since the docs will be combined, it's crucial to add the relevant
+[product badges](styleguide.md#product-badges) for all EE documentation, so that
+we can discern which features belong to which tier.
+
+### EE specific lines check
+
+There's a special test in place
+([`ee_specific_check.rb`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/scripts/ee_specific_check/ee_specific_check.rb)),
+which, among others, checks and prevents creating/editing new files and directories
+in EE under `doc/`.
+
+We have a long list of documentation paths that are either whitelisted or not.
+Paths in the whitelist (not commented out) will not be subject to the test,
+which means you are allowed to create/change docs content in EE for the time
+being. The goal is to not have any doc whitelisted.
+
+At the time of this writing, the only items left to be aligned are the API docs:
+
+- `doc/api/*` ([issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/60045) / [merge request](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27491))
+
+Eventually, once all docs are aligned, we'll remove any doc reference from that
+script, so it catches everything.
+
## Changing document location
Changing a document's location requires specific steps to be followed to ensure that
@@ -438,6 +489,10 @@ Currently, the following tests are in place:
As CE is merged into EE once a day, it's important to avoid merge conflicts.
Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is
essential to avoid them.
+1. [`ee-files-location-check`/`ee-specific-lines-check`](#ee-specific-lines-check) (runs on EE only):
+ This test ensures that no new files/directories are created/changed in EE.
+ All docs should be submitted in CE instead, regardless the tier they are on.
+ This is for the [single codebase](#single-codebase) effort.
1. In a full pipeline, tests for [`/help`](#gitlab-help-tests).
### Linting
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 9853b38b8e9..bc472bb5b0a 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -446,6 +446,28 @@ The disadvantage of this:
port `render_if_exists` to CE.
- If we have typos in the partial name, it would be silently ignored.
+
+##### Caveats
+
+The `render_if_exists` view path argument must be relative to `app/views/` and `ee/app/views`.
+Resolving an EE template path that is relative to the CE view path will not work.
+
+```haml
+- # app/views/projects/index.html.haml
+
+= render_if_exists 'button' # Will not render `ee/app/views/projects/_button` and will quietly fail
+= render_if_exists 'projects/button' # Will render `ee/app/views/projects/_button`
+```
+
+You should not explicitly set render options like `partial` or provide a `locals` hash.
+The first argument should be a path string and the second can be a hash replacing `locals`.
+
+```ruby
+render partial: 'projects/button', locals: { project: project }
+# becomes
+render_if_exists 'projects/button', project: project
+```
+
#### Using `render_ce`
For `render` and `render_if_exists`, they search for the EE partial first,
@@ -888,7 +910,7 @@ information on managing page-specific javascript within EE.
### script tag
#### Child Component only used in EE
-To seperate Vue template differences we should [async import the components](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components).
+To separate Vue template differences we should [async import the components](https://vuejs.org/v2/guide/components-dynamic-async.html#Async-Components).
Doing this allows for us to load the correct component in EE whilst in CE
we can load a empty component that renders nothing. This code **should**
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 5f6123b5f9b..8e06aa5d173 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -43,9 +43,9 @@ new Vue({
Read more about [Vue Apollo][vue-apollo] in the [Vue Apollo documentation][vue-apollo-docs].
-### Local state with `apollo-link-state`
+### Local state with Apollo
-It is possible to use our Apollo setup with [apollo-link-state][apollo-link-state] by passing
+It is possible to manage an application state with Apollo by passing
in a resolvers object when creating the default client. The default state can be set by writing
to the cache after setting up the default client.
@@ -76,6 +76,8 @@ const apolloProvider = new VueApollo({
});
```
+Read more about local state management with Apollo in the [Vue Apollo documentation](https://vue-apollo.netlify.com/guide/local-state.html#local-state).
+
### Testing
With [Vue test utils][vue-test-utils] it is easy to quickly test components that
@@ -92,6 +94,8 @@ it('tests apollo component', () => {
});
```
+Another possible way is testing queries with mocked GraphQL schema. Read more about this way in [Vue Apollo testing documentation](https://vue-apollo.netlify.com/guide/testing.html#tests-with-mocked-graqhql-schema)
+
## Usage outside of Vue
It is also possible to use GraphQL outside of Vue by directly importing
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 7d52cac5f7e..bf248b7f8af 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -186,7 +186,7 @@ Remember that actions only describe that something happened, they don't describe
state.users.push(user);
},
[types.REQUEST_ADD_USER_ERROR](state, error) {
- state.isAddingUser = true;
+ state.isAddingUser = false;
state.errorAddingUser = error;
},
};
@@ -231,7 +231,7 @@ The store should be included in the main component of your application:
```javascript
// app.vue
- import store from 'store'; // it will include the index.js file
+ import store from './store'; // it will include the index.js file
export default {
name: 'application',
diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md
index 3271f9a7fb3..82b09cc0224 100644
--- a/doc/development/feature_flags.md
+++ b/doc/development/feature_flags.md
@@ -108,11 +108,11 @@ so we make sure behavior under feature flag doesn't go untested in some non-spec
contexts.
Whenever a feature flag is present, make sure to test _both_ states of the
-feature flag. You can stub a feature flag as follows:
+feature flag.
-```ruby
-stub_feature_flags(my_feature_flag: false)
-```
+See the
+[testing guide](testing_guide/best_practices.html#feature-flags-in-tests)
+for information and examples on how to stub feature flags in tests.
## Enabling a feature flag (in development)
diff --git a/doc/development/gitlab_architecture_diagram.png b/doc/development/gitlab_architecture_diagram.png
deleted file mode 100644
index 90e27d5462a..00000000000
--- a/doc/development/gitlab_architecture_diagram.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md
index b9dc3797e5b..cf78b792ffd 100644
--- a/doc/development/go_guide/index.md
+++ b/doc/development/go_guide/index.md
@@ -40,7 +40,7 @@ of possible security breaches in our code:
- SQL injections
Remember to run
-[SAST](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html)
+[SAST](https://docs.gitlab.com/ee/user/application_security/sast/index)
**[ULTIMATE]** on your project (or at least the [gosec
analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/gosec)),
and to follow our [Security
@@ -96,7 +96,7 @@ dependency should be argued in the merge request, as per our [Approval
Guidelines](../code_review.md#approval-guidelines). Both [License
Management](https://docs.gitlab.com/ee/user/project/merge_requests/license_management.html)
**[ULTIMATE]** and [Dependency
-Scanning](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html)
+Scanning](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index)
**[ULTIMATE]** should be activated on all projects to ensure new dependencies
security status and license compatibility.
diff --git a/doc/development/rolling_out_changes_using_feature_flags.md b/doc/development/rolling_out_changes_using_feature_flags.md
index 8d35a4ecee2..8e4e07c4319 100644
--- a/doc/development/rolling_out_changes_using_feature_flags.md
+++ b/doc/development/rolling_out_changes_using_feature_flags.md
@@ -65,15 +65,14 @@ the worst case scenario, which we should optimise for, our total cost is now 20.
If we had used a feature flag, things would have been very different. We don't
need to revert a release, and because feature flags are disabled by default we
don't need to revert and pick any Git commits. In fact, all we have to do is
-disable the feature, and _maybe_ perform some cleanup. Let's say that the cost
-of this is 1. In this case, our best case cost is 11: 10 to build the feature,
-and 1 to add the feature flag. The worst case cost is now 12: 10 to build the
-feature, 1 to add the feature flag, and 1 to disable it.
+disable the feature, and in the worst case, perform cleanup. Let's say that
+the cost of this is 2. In this case, our best case cost is 11: 10 to build the
+feature, and 1 to add the feature flag. The worst case cost is now 13: 10 to
+build the feature, 1 to add the feature flag, and 2 to disable and clean up.
Here we can see that in the best case scenario the work necessary is only a tiny
bit more compared to not using a feature flag. Meanwhile, the process of
-reverting our changes has been made significantly cheaper, to the point of being
-trivial.
+reverting our changes has been made significantly and reliably cheaper.
In other words, feature flags do not slow down the development process. Instead,
they speed up the process as managing incidents now becomes _much_ easier. Once
diff --git a/doc/development/session.md b/doc/development/session.md
new file mode 100644
index 00000000000..9edce3dbda0
--- /dev/null
+++ b/doc/development/session.md
@@ -0,0 +1,65 @@
+# Accessing session data
+
+Session data in GitLab is stored in Redis and can be accessed in a variety of ways.
+
+During a web request, for example:
+
+- Rails provides access to the session from within controllers through [`ActionDispatch::Session`](https://guides.rubyonrails.org/action_controller_overview.html#session).
+- Outside of controllers, it is possible to access the session through `Gitlab::Session`.
+
+Outside of a web request it is still possible to access sessions stored in Redis. For example:
+
+- Session IDs and contents can be [looked up directly in Redis](#redis).
+- Data about the UserAgent associated with the session can be accessed through `ActiveSession`.
+
+When storing values in a session it is best to:
+
+- Use simple primitives and avoid storing objects to avoid marshaling complications.
+- Clean up after unneeded variables to keep memory usage in Redis down.
+
+## Gitlab::Session
+
+Sometimes you might want to persist data in the session instead of another store like the database. `Gitlab::Session` lets you access this without passing the session around extensively. For example, you could access it from within a policy without having to pass the session through to each place permissions are checked from.
+
+The session has a hash-like interface, just like when using it from a controller. There is also `NamespacedSessionStore` for storing key-value data in a hash.
+
+```ruby
+# Lookup a value stored in the current session
+Gitlab::Session.current[:my_feature]
+
+# Modify the current session stored in redis
+Gitlab::Session.current[:my_feature] = value
+
+# Store key-value data namespaced under a key
+Gitlab::NamespacedSessionStore.new(:my_feature)[some_key] = value
+
+# Set the session for a block of code, such as for tests
+Gitlab::Session.with_session(my_feature: value) do
+ # Code that uses Session.current[:my_feature]
+end
+```
+
+## Redis
+
+Session data can be accessed directly through Redis. This can let you check up on a browser session when debugging.
+
+```ruby
+# Get a list of sessions
+session_ids = Gitlab::Redis::SharedState.with do |redis|
+ redis.smembers("#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user.id}")
+end
+
+# Retrieve a specific session
+session_data = Gitlab::Redis::SharedState.with { |redis| redis.get("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}") }
+Marshal.load(session_data)
+```
+
+## Getting device information with ActiveSession
+
+The [**Active Sessions** page on a user's profile](../user/profile/active_sessions.md) displays information about the device used to access each session. The methods used there to list sessions can also be useful for development.
+
+```ruby
+# Get list of sessions for a given user
+# Includes session_id and data from the UserAgent
+ActiveSession.list(user)
+```
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index e41148360f2..63ec9755462 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -240,6 +240,36 @@ it 'is overdue' do
end
```
+### Feature flags in tests
+
+All feature flags are stubbed to be enabled by default in our Ruby-based
+tests.
+
+To disable a feature flag in a test, use the `stub_feature_flags`
+helper. For example, to globally disable the `ci_live_trace` feature
+flag in a test:
+
+```ruby
+stub_feature_flags(ci_live_trace: false)
+
+Feature.enabled?(:ci_live_trace) # => false
+```
+
+If you wish to set up a test where a feature flag is disabled for some
+actors and not others, you can specify this in options passed to the
+helper. For example, to disable the `ci_live_trace` feature flag for a
+specifc project:
+
+```ruby
+project1, project2 = build_list(:project, 2)
+
+# Feature will only be disabled for project1
+stub_feature_flags(ci_live_trace: { enabled: false, thing: project1 })
+
+Feature.enabled?(:ci_live_trace, project1) # => false
+Feature.enabled?(:ci_live_trace, project2) # => true
+```
+
### Pristine test environments
The code exercised by a single GitLab test may access and modify many items of
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 7ad33f05f77..f34793c11f4 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -78,17 +78,27 @@ subgraph CNG-mirror pipeline
**Additional notes:**
-- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using
- [GitLab's Kubernetes integration][gitlab-k8s-integration]. This basically
- allows to have a link to the Review App directly from the merge request
- widget.
-- If the Review App deployment fails, you can simply retry it (there's no need
- to run the [`review-stop`][gitlab-ci-yml] job first).
+- If the `review-deploy` job keep failing (note that we already retry it twice),
+ please post a message in the `#quality` channel and/or create a ~Quality ~bug
+ issue with a link to your merge request. Note that the deployment failure can
+ reveal an actual problem introduced in your merge request (i.e. this isn't
+ necessarily a transient failure)!
+- If the `review-qa-smoke` job keep failing (note that we already retry it twice),
+ please check the job's logs: you could discover an actual problem introduced in
+ your merge request. You can also download the artifacts to see screenshots of
+ the page at the time the failures occurred. If you don't find the cause of the
+ failure or if it seems unrelated to your change, please post a message in the
+ `#quality` channel and/or create a ~Quality ~bug issue with a link to your
+ merge request.
- The manual [`review-stop`][gitlab-ci-yml] in the `test` stage can be used to
stop a Review App manually, and is also started by GitLab once a merge
request's branch is deleted after being merged.
- Review Apps are cleaned up regularly via a pipeline schedule that runs
the [`schedule:review-cleanup`][gitlab-ci-yml] job.
+- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using
+ [GitLab's Kubernetes integration][gitlab-k8s-integration]. This basically
+ allows to have a link to the Review App directly from the merge request
+ widget.
## QA runs
diff --git a/doc/development/ux_guide/img/animation-autoscroll.gif b/doc/development/ux_guide/img/animation-autoscroll.gif
deleted file mode 100644
index 155b0234c64..00000000000
--- a/doc/development/ux_guide/img/animation-autoscroll.gif
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/animation-dropdown.gif b/doc/development/ux_guide/img/animation-dropdown.gif
deleted file mode 100644
index c9b31d26165..00000000000
--- a/doc/development/ux_guide/img/animation-dropdown.gif
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/animation-hover.gif b/doc/development/ux_guide/img/animation-hover.gif
deleted file mode 100644
index 37ad9c76828..00000000000
--- a/doc/development/ux_guide/img/animation-hover.gif
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/animation-quickupdate.gif b/doc/development/ux_guide/img/animation-quickupdate.gif
deleted file mode 100644
index 8db70bc3d24..00000000000
--- a/doc/development/ux_guide/img/animation-quickupdate.gif
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/animation-reorder.gif b/doc/development/ux_guide/img/animation-reorder.gif
deleted file mode 100644
index ccdeb3d396f..00000000000
--- a/doc/development/ux_guide/img/animation-reorder.gif
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-close--active.png b/doc/development/ux_guide/img/button-close--active.png
deleted file mode 100644
index 97a5301fb91..00000000000
--- a/doc/development/ux_guide/img/button-close--active.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-close--hover.png b/doc/development/ux_guide/img/button-close--hover.png
deleted file mode 100644
index 6b8fdf5695b..00000000000
--- a/doc/development/ux_guide/img/button-close--hover.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-close--resting.png b/doc/development/ux_guide/img/button-close--resting.png
deleted file mode 100644
index 5679b51687c..00000000000
--- a/doc/development/ux_guide/img/button-close--resting.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-danger--active.png b/doc/development/ux_guide/img/button-danger--active.png
deleted file mode 100644
index 6a9aab0fcc2..00000000000
--- a/doc/development/ux_guide/img/button-danger--active.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-danger--hover.png b/doc/development/ux_guide/img/button-danger--hover.png
deleted file mode 100644
index 13e21c28779..00000000000
--- a/doc/development/ux_guide/img/button-danger--hover.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-danger--resting.png b/doc/development/ux_guide/img/button-danger--resting.png
deleted file mode 100644
index 0ff192bc463..00000000000
--- a/doc/development/ux_guide/img/button-danger--resting.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-info--active.png b/doc/development/ux_guide/img/button-info--active.png
deleted file mode 100644
index 12ecdc72a31..00000000000
--- a/doc/development/ux_guide/img/button-info--active.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-info--hover.png b/doc/development/ux_guide/img/button-info--hover.png
deleted file mode 100644
index 3bf93bf2b32..00000000000
--- a/doc/development/ux_guide/img/button-info--hover.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-info--resting.png b/doc/development/ux_guide/img/button-info--resting.png
deleted file mode 100644
index a37a37033bf..00000000000
--- a/doc/development/ux_guide/img/button-info--resting.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-primary.png b/doc/development/ux_guide/img/button-primary.png
deleted file mode 100644
index eda5ed84aec..00000000000
--- a/doc/development/ux_guide/img/button-primary.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-secondary.png b/doc/development/ux_guide/img/button-secondary.png
deleted file mode 100644
index 26d4e8cf43d..00000000000
--- a/doc/development/ux_guide/img/button-secondary.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-spam--active.png b/doc/development/ux_guide/img/button-spam--active.png
deleted file mode 100644
index a9e115f49c1..00000000000
--- a/doc/development/ux_guide/img/button-spam--active.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-spam--hover.png b/doc/development/ux_guide/img/button-spam--hover.png
deleted file mode 100644
index 3b2c16430a6..00000000000
--- a/doc/development/ux_guide/img/button-spam--hover.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-spam--resting.png b/doc/development/ux_guide/img/button-spam--resting.png
deleted file mode 100644
index 4f9f18ca68a..00000000000
--- a/doc/development/ux_guide/img/button-spam--resting.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success--active.png b/doc/development/ux_guide/img/button-success--active.png
deleted file mode 100644
index b99f6f5e70e..00000000000
--- a/doc/development/ux_guide/img/button-success--active.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success--hover.png b/doc/development/ux_guide/img/button-success--hover.png
deleted file mode 100644
index 0d0a61c679a..00000000000
--- a/doc/development/ux_guide/img/button-success--hover.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success--resting.png b/doc/development/ux_guide/img/button-success--resting.png
deleted file mode 100644
index 53b955c650a..00000000000
--- a/doc/development/ux_guide/img/button-success--resting.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success-secondary--active.png b/doc/development/ux_guide/img/button-success-secondary--active.png
deleted file mode 100644
index 333a91f2217..00000000000
--- a/doc/development/ux_guide/img/button-success-secondary--active.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success-secondary--hover.png b/doc/development/ux_guide/img/button-success-secondary--hover.png
deleted file mode 100644
index 0cce59212e3..00000000000
--- a/doc/development/ux_guide/img/button-success-secondary--hover.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-success-secondary--resting.png b/doc/development/ux_guide/img/button-success-secondary--resting.png
deleted file mode 100644
index 2779a4949f8..00000000000
--- a/doc/development/ux_guide/img/button-success-secondary--resting.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-warning--active.png b/doc/development/ux_guide/img/button-warning--active.png
deleted file mode 100644
index f5760cd7c12..00000000000
--- a/doc/development/ux_guide/img/button-warning--active.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-warning--hover.png b/doc/development/ux_guide/img/button-warning--hover.png
deleted file mode 100644
index a1f4c5cbcc6..00000000000
--- a/doc/development/ux_guide/img/button-warning--hover.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/button-warning--resting.png b/doc/development/ux_guide/img/button-warning--resting.png
deleted file mode 100644
index 3d62fed5930..00000000000
--- a/doc/development/ux_guide/img/button-warning--resting.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/color-blue.png b/doc/development/ux_guide/img/color-blue.png
deleted file mode 100644
index 77c1a2cab31..00000000000
--- a/doc/development/ux_guide/img/color-blue.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/color-green.png b/doc/development/ux_guide/img/color-green.png
deleted file mode 100644
index 51600584c96..00000000000
--- a/doc/development/ux_guide/img/color-green.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/color-grey.png b/doc/development/ux_guide/img/color-grey.png
deleted file mode 100644
index f0f0b9d80bb..00000000000
--- a/doc/development/ux_guide/img/color-grey.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/color-orange.png b/doc/development/ux_guide/img/color-orange.png
deleted file mode 100644
index f16435c0a64..00000000000
--- a/doc/development/ux_guide/img/color-orange.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/color-red.png b/doc/development/ux_guide/img/color-red.png
deleted file mode 100644
index 5008e75da78..00000000000
--- a/doc/development/ux_guide/img/color-red.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/color-textprimary.png b/doc/development/ux_guide/img/color-textprimary.png
deleted file mode 100644
index 90f2821f0cf..00000000000
--- a/doc/development/ux_guide/img/color-textprimary.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/color-textsecondary.png b/doc/development/ux_guide/img/color-textsecondary.png
deleted file mode 100644
index 61cb8a13c45..00000000000
--- a/doc/development/ux_guide/img/color-textsecondary.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-alerts.png b/doc/development/ux_guide/img/components-alerts.png
deleted file mode 100644
index 66a43ac69e1..00000000000
--- a/doc/development/ux_guide/img/components-alerts.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-anchorlinks.png b/doc/development/ux_guide/img/components-anchorlinks.png
deleted file mode 100644
index bd8d30f5905..00000000000
--- a/doc/development/ux_guide/img/components-anchorlinks.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-contentblock.png b/doc/development/ux_guide/img/components-contentblock.png
deleted file mode 100644
index 58d87729701..00000000000
--- a/doc/development/ux_guide/img/components-contentblock.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-counts.png b/doc/development/ux_guide/img/components-counts.png
deleted file mode 100644
index 19280e988a0..00000000000
--- a/doc/development/ux_guide/img/components-counts.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-coverblock.png b/doc/development/ux_guide/img/components-coverblock.png
deleted file mode 100644
index 61160de5613..00000000000
--- a/doc/development/ux_guide/img/components-coverblock.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-dateexact.png b/doc/development/ux_guide/img/components-dateexact.png
deleted file mode 100644
index cc1fb8216bf..00000000000
--- a/doc/development/ux_guide/img/components-dateexact.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-daterelative.png b/doc/development/ux_guide/img/components-daterelative.png
deleted file mode 100644
index 4954dfb51b3..00000000000
--- a/doc/development/ux_guide/img/components-daterelative.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-dropdown.png b/doc/development/ux_guide/img/components-dropdown.png
deleted file mode 100644
index 7f9a701c089..00000000000
--- a/doc/development/ux_guide/img/components-dropdown.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-fileholder.png b/doc/development/ux_guide/img/components-fileholder.png
deleted file mode 100644
index 5bf8565346a..00000000000
--- a/doc/development/ux_guide/img/components-fileholder.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-horizontalform.png b/doc/development/ux_guide/img/components-horizontalform.png
deleted file mode 100644
index e6cbc69d20a..00000000000
--- a/doc/development/ux_guide/img/components-horizontalform.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-listinsidepanel.png b/doc/development/ux_guide/img/components-listinsidepanel.png
deleted file mode 100644
index 6b773a19954..00000000000
--- a/doc/development/ux_guide/img/components-listinsidepanel.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-listwithavatar.png b/doc/development/ux_guide/img/components-listwithavatar.png
deleted file mode 100644
index f6db575433c..00000000000
--- a/doc/development/ux_guide/img/components-listwithavatar.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-listwithhover.png b/doc/development/ux_guide/img/components-listwithhover.png
deleted file mode 100644
index 0826848ff34..00000000000
--- a/doc/development/ux_guide/img/components-listwithhover.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-panels.png b/doc/development/ux_guide/img/components-panels.png
deleted file mode 100644
index c1391ca07e5..00000000000
--- a/doc/development/ux_guide/img/components-panels.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencehover.png b/doc/development/ux_guide/img/components-referencehover.png
deleted file mode 100644
index af5405d3e0b..00000000000
--- a/doc/development/ux_guide/img/components-referencehover.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referenceissues.png b/doc/development/ux_guide/img/components-referenceissues.png
deleted file mode 100644
index 4e175dc169d..00000000000
--- a/doc/development/ux_guide/img/components-referenceissues.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencelabels.png b/doc/development/ux_guide/img/components-referencelabels.png
deleted file mode 100644
index 29a985bbaa0..00000000000
--- a/doc/development/ux_guide/img/components-referencelabels.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencemilestone.png b/doc/development/ux_guide/img/components-referencemilestone.png
deleted file mode 100644
index 47c76a9d60f..00000000000
--- a/doc/development/ux_guide/img/components-referencemilestone.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencemrs.png b/doc/development/ux_guide/img/components-referencemrs.png
deleted file mode 100644
index 9a5032a1516..00000000000
--- a/doc/development/ux_guide/img/components-referencemrs.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-referencepeople.png b/doc/development/ux_guide/img/components-referencepeople.png
deleted file mode 100644
index f9ef11be853..00000000000
--- a/doc/development/ux_guide/img/components-referencepeople.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-rowcontentblock.png b/doc/development/ux_guide/img/components-rowcontentblock.png
deleted file mode 100644
index c66a50f9564..00000000000
--- a/doc/development/ux_guide/img/components-rowcontentblock.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-searchbox.png b/doc/development/ux_guide/img/components-searchbox.png
deleted file mode 100644
index 5c19024bfb0..00000000000
--- a/doc/development/ux_guide/img/components-searchbox.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-searchboxscoped.png b/doc/development/ux_guide/img/components-searchboxscoped.png
deleted file mode 100644
index d4a35977658..00000000000
--- a/doc/development/ux_guide/img/components-searchboxscoped.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-simplelist.png b/doc/development/ux_guide/img/components-simplelist.png
deleted file mode 100644
index 8d11c674e84..00000000000
--- a/doc/development/ux_guide/img/components-simplelist.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-table.png b/doc/development/ux_guide/img/components-table.png
deleted file mode 100644
index cedc55758a9..00000000000
--- a/doc/development/ux_guide/img/components-table.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/components-verticalform.png b/doc/development/ux_guide/img/components-verticalform.png
deleted file mode 100644
index 489ae6f862f..00000000000
--- a/doc/development/ux_guide/img/components-verticalform.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-default.png b/doc/development/ux_guide/img/cursors-default.png
deleted file mode 100644
index c188ec4e351..00000000000
--- a/doc/development/ux_guide/img/cursors-default.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-ibeam.png b/doc/development/ux_guide/img/cursors-ibeam.png
deleted file mode 100644
index 86f28639982..00000000000
--- a/doc/development/ux_guide/img/cursors-ibeam.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-move.png b/doc/development/ux_guide/img/cursors-move.png
deleted file mode 100644
index a9c610eaa88..00000000000
--- a/doc/development/ux_guide/img/cursors-move.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-panclosed.png b/doc/development/ux_guide/img/cursors-panclosed.png
deleted file mode 100644
index 6d247a765ac..00000000000
--- a/doc/development/ux_guide/img/cursors-panclosed.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-panopened.png b/doc/development/ux_guide/img/cursors-panopened.png
deleted file mode 100644
index 76f2eeda831..00000000000
--- a/doc/development/ux_guide/img/cursors-panopened.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/cursors-pointer.png b/doc/development/ux_guide/img/cursors-pointer.png
deleted file mode 100644
index d86dd955fa7..00000000000
--- a/doc/development/ux_guide/img/cursors-pointer.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/features-contextualnav.png b/doc/development/ux_guide/img/features-contextualnav.png
deleted file mode 100644
index aa816776fad..00000000000
--- a/doc/development/ux_guide/img/features-contextualnav.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/features-emptystates.png b/doc/development/ux_guide/img/features-emptystates.png
deleted file mode 100644
index 50f31f5e523..00000000000
--- a/doc/development/ux_guide/img/features-emptystates.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/features-filters.png b/doc/development/ux_guide/img/features-filters.png
deleted file mode 100644
index 41db76db938..00000000000
--- a/doc/development/ux_guide/img/features-filters.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/features-globalnav.png b/doc/development/ux_guide/img/features-globalnav.png
deleted file mode 100644
index 73294d1b524..00000000000
--- a/doc/development/ux_guide/img/features-globalnav.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/harry-robison.png b/doc/development/ux_guide/img/harry-robison.png
deleted file mode 100644
index 702a8b02262..00000000000
--- a/doc/development/ux_guide/img/harry-robison.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-add.png b/doc/development/ux_guide/img/icon-add.png
deleted file mode 100644
index f66525cc1b4..00000000000
--- a/doc/development/ux_guide/img/icon-add.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-close.png b/doc/development/ux_guide/img/icon-close.png
deleted file mode 100644
index af6c30ebe6a..00000000000
--- a/doc/development/ux_guide/img/icon-close.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-edit.png b/doc/development/ux_guide/img/icon-edit.png
deleted file mode 100644
index b9649f4aeec..00000000000
--- a/doc/development/ux_guide/img/icon-edit.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-notification.png b/doc/development/ux_guide/img/icon-notification.png
deleted file mode 100644
index 5cf8f8ab59a..00000000000
--- a/doc/development/ux_guide/img/icon-notification.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-rss.png b/doc/development/ux_guide/img/icon-rss.png
deleted file mode 100644
index 7e2987a2656..00000000000
--- a/doc/development/ux_guide/img/icon-rss.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-spec.png b/doc/development/ux_guide/img/icon-spec.png
deleted file mode 100644
index 5bb85c5be98..00000000000
--- a/doc/development/ux_guide/img/icon-spec.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-subscribe.png b/doc/development/ux_guide/img/icon-subscribe.png
deleted file mode 100644
index 7e2f5e6a1c6..00000000000
--- a/doc/development/ux_guide/img/icon-subscribe.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/icon-trash.png b/doc/development/ux_guide/img/icon-trash.png
deleted file mode 100644
index bc46638fb2e..00000000000
--- a/doc/development/ux_guide/img/icon-trash.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustration-size-large-horizontal.png b/doc/development/ux_guide/img/illustration-size-large-horizontal.png
deleted file mode 100644
index 8aa835adccc..00000000000
--- a/doc/development/ux_guide/img/illustration-size-large-horizontal.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustration-size-large-vertical.png b/doc/development/ux_guide/img/illustration-size-large-vertical.png
deleted file mode 100644
index 813b6a065e5..00000000000
--- a/doc/development/ux_guide/img/illustration-size-large-vertical.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustration-size-medium.png b/doc/development/ux_guide/img/illustration-size-medium.png
deleted file mode 100644
index 55cfe1dcb91..00000000000
--- a/doc/development/ux_guide/img/illustration-size-medium.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustration-size-small.png b/doc/development/ux_guide/img/illustration-size-small.png
deleted file mode 100644
index 0124f58f48e..00000000000
--- a/doc/development/ux_guide/img/illustration-size-small.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-border-radius.png b/doc/development/ux_guide/img/illustrations-border-radius.png
deleted file mode 100644
index 4e2fef5c7f5..00000000000
--- a/doc/development/ux_guide/img/illustrations-border-radius.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-caps-do.png b/doc/development/ux_guide/img/illustrations-caps-do.png
deleted file mode 100644
index f1030769b94..00000000000
--- a/doc/development/ux_guide/img/illustrations-caps-do.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-caps-don't.png b/doc/development/ux_guide/img/illustrations-caps-don't.png
deleted file mode 100644
index ab7abcaaf6f..00000000000
--- a/doc/development/ux_guide/img/illustrations-caps-don't.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-color-grey.png b/doc/development/ux_guide/img/illustrations-color-grey.png
deleted file mode 100644
index 63855026c2b..00000000000
--- a/doc/development/ux_guide/img/illustrations-color-grey.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-color-orange.png b/doc/development/ux_guide/img/illustrations-color-orange.png
deleted file mode 100644
index 96765c8c28c..00000000000
--- a/doc/development/ux_guide/img/illustrations-color-orange.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-color-purple.png b/doc/development/ux_guide/img/illustrations-color-purple.png
deleted file mode 100644
index 745d2c853ba..00000000000
--- a/doc/development/ux_guide/img/illustrations-color-purple.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-geometric.png b/doc/development/ux_guide/img/illustrations-geometric.png
deleted file mode 100644
index 33f05547bac..00000000000
--- a/doc/development/ux_guide/img/illustrations-geometric.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-palette-oragne.png b/doc/development/ux_guide/img/illustrations-palette-oragne.png
deleted file mode 100644
index 15f35912646..00000000000
--- a/doc/development/ux_guide/img/illustrations-palette-oragne.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/illustrations-palette-purple.png b/doc/development/ux_guide/img/illustrations-palette-purple.png
deleted file mode 100644
index e0f5839705e..00000000000
--- a/doc/development/ux_guide/img/illustrations-palette-purple.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/james-mackey.png b/doc/development/ux_guide/img/james-mackey.png
deleted file mode 100644
index f51a45c437b..00000000000
--- a/doc/development/ux_guide/img/james-mackey.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/karolina-plaskaty.png b/doc/development/ux_guide/img/karolina-plaskaty.png
deleted file mode 100644
index d1c9528dd5a..00000000000
--- a/doc/development/ux_guide/img/karolina-plaskaty.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/matthieu-poirier.png b/doc/development/ux_guide/img/matthieu-poirier.png
deleted file mode 100644
index 0ecc2d670d6..00000000000
--- a/doc/development/ux_guide/img/matthieu-poirier.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/modals-general-confimation-dialog.png b/doc/development/ux_guide/img/modals-general-confimation-dialog.png
deleted file mode 100644
index 4ea0ea10ca7..00000000000
--- a/doc/development/ux_guide/img/modals-general-confimation-dialog.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/modals-layout-for-modals.png b/doc/development/ux_guide/img/modals-layout-for-modals.png
deleted file mode 100644
index c481edd8250..00000000000
--- a/doc/development/ux_guide/img/modals-layout-for-modals.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/modals-special-confimation-dialog.png b/doc/development/ux_guide/img/modals-special-confimation-dialog.png
deleted file mode 100644
index d966010158b..00000000000
--- a/doc/development/ux_guide/img/modals-special-confimation-dialog.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/modals-three-buttons.png b/doc/development/ux_guide/img/modals-three-buttons.png
deleted file mode 100644
index 157d1b650bf..00000000000
--- a/doc/development/ux_guide/img/modals-three-buttons.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/monospacefont-sample.png b/doc/development/ux_guide/img/monospacefont-sample.png
deleted file mode 100644
index 1cd290b713c..00000000000
--- a/doc/development/ux_guide/img/monospacefont-sample.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/nazim-ramesh.png b/doc/development/ux_guide/img/nazim-ramesh.png
deleted file mode 100644
index dad2b37010b..00000000000
--- a/doc/development/ux_guide/img/nazim-ramesh.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/popover-placement-above.png b/doc/development/ux_guide/img/popover-placement-above.png
deleted file mode 100644
index 84c9c878ec2..00000000000
--- a/doc/development/ux_guide/img/popover-placement-above.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/popover-placement-below.png b/doc/development/ux_guide/img/popover-placement-below.png
deleted file mode 100644
index f6f18199ab6..00000000000
--- a/doc/development/ux_guide/img/popover-placement-below.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/skeleton-loading.gif b/doc/development/ux_guide/img/skeleton-loading.gif
deleted file mode 100644
index 5877139171d..00000000000
--- a/doc/development/ux_guide/img/skeleton-loading.gif
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/sourcesanspro-sample.png b/doc/development/ux_guide/img/sourcesanspro-sample.png
deleted file mode 100644
index f7ecf0c7c66..00000000000
--- a/doc/development/ux_guide/img/sourcesanspro-sample.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/steven-lyons.png b/doc/development/ux_guide/img/steven-lyons.png
deleted file mode 100644
index 2efe1d0b168..00000000000
--- a/doc/development/ux_guide/img/steven-lyons.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-contentitemtitle.png b/doc/development/ux_guide/img/surfaces-contentitemtitle.png
deleted file mode 100644
index f6cd212ecfd..00000000000
--- a/doc/development/ux_guide/img/surfaces-contentitemtitle.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-header.png b/doc/development/ux_guide/img/surfaces-header.png
deleted file mode 100644
index ba616388003..00000000000
--- a/doc/development/ux_guide/img/surfaces-header.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-systeminformationblock.png b/doc/development/ux_guide/img/surfaces-systeminformationblock.png
deleted file mode 100644
index f3313add2b8..00000000000
--- a/doc/development/ux_guide/img/surfaces-systeminformationblock.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/surfaces-ux.png b/doc/development/ux_guide/img/surfaces-ux.png
deleted file mode 100644
index eaa7f70c0c7..00000000000
--- a/doc/development/ux_guide/img/surfaces-ux.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/tooltip-placement.png b/doc/development/ux_guide/img/tooltip-placement.png
deleted file mode 100644
index da49c192878..00000000000
--- a/doc/development/ux_guide/img/tooltip-placement.png
+++ /dev/null
Binary files differ
diff --git a/doc/development/ux_guide/img/tooltip-usage.png b/doc/development/ux_guide/img/tooltip-usage.png
deleted file mode 100644
index 4f5884c4b48..00000000000
--- a/doc/development/ux_guide/img/tooltip-usage.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index aa008d6f768..28ba20bec09 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -27,9 +27,11 @@ The following are guides to basic GitLab functionality:
## Git basics
-If you're unfamiliar with the command line, these resources will help:
+If you're familiar with Git on the command line, you can interact with your GitLab projects just as you would with any other Git repository.
+
+These resources will help get further acclimated to working on the command line.
-- [Command line basics](command-line-commands.md), for those unfamiliar with the command line interface.
- [Start using Git on the command line](start-using-git.md), for some simple Git commands.
+- [Command line basics](command-line-commands.md), to create and edit files using the command line.
More Git resources are available at GitLab's [Git documentation](../topics/git/index.md).
diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index a0111be0767..1cf883679d7 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -13,10 +13,12 @@ button (you'll have to paste it on your shell in the next step).
![Copy the HTTPS or SSH](img/project_clone_url.png)
-## On the command line
+## Working with project files on the command line
This section has examples of some basic shell commands that you might find useful. For more information, search the web for _bash commands_.
+Alternatively, you can edit files using your choice of editor (IDE) or the GitLab user interface.
+
### Clone your project
Go to your computer's shell and type the following command with your SSH or HTTPS URL:
diff --git a/doc/gitlab-basics/img/new_issue_button.png b/doc/gitlab-basics/img/new_issue_button.png
deleted file mode 100644
index 3b113471f0c..00000000000
--- a/doc/gitlab-basics/img/new_issue_button.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/img/new_issue_page.png b/doc/gitlab-basics/img/new_issue_page.png
deleted file mode 100644
index ce3e60df276..00000000000
--- a/doc/gitlab-basics/img/new_issue_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/img/public_file_link.png b/doc/gitlab-basics/img/public_file_link.png
deleted file mode 100644
index f60df6807f4..00000000000
--- a/doc/gitlab-basics/img/public_file_link.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index e30afdf8a40..b3c5d32f2f5 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -71,6 +71,18 @@ git config --global --list
Start using Git via the command line with the most basic
commands as described below.
+## Initialize a local directory for Git version control
+
+If you have an existing local directory that you want to *initialize* for version control, use the `init` command to instruct Git to begin tracking the directory:
+
+```bash
+git init
+```
+
+This creates a `.git` directory that contains the Git configuration files.
+
+Once the directory has been initialized, you can [add a remote repository](#add-a-remote-repository) and [send changes to GitLab.com](#send-changes-to-gitlabcom). View the instructions on [Create a project](../gitlab-basics/create-project.html#push-to-create-a-new-project) to create a new project on GitLab with your changes.
+
### Clone a repository
To start working locally on an existing remote repository,
@@ -140,6 +152,16 @@ To view your remote repositories, type:
git remote -v
```
+### Add a remote repository
+
+To add a link to a remote repository:
+
+```bash
+git remote add SOURCE-NAME REPOSITORY-PATH
+```
+
+You'll use this source name every time you [push changes to GitLab.com](#send-changes-to-gitlabcom), so use something easy to remember and type.
+
### Create a branch
To create a branch, type the following (spaces won't be recognized in the branch name, so you will need to use a hyphen or underscore):
@@ -193,7 +215,7 @@ git commit -m "COMMENT TO DESCRIBE THE INTENTION OF THE COMMIT"
NOTE: **Note:**
The `.` character typically means _all_ in Git.
-### Send changes to gitlab.com
+### Send changes to GitLab.com
To push all local commits to the remote repository:
diff --git a/doc/install/aws/img/add_tags.png b/doc/install/aws/img/add_tags.png
deleted file mode 100644
index 3572cd5daa1..00000000000
--- a/doc/install/aws/img/add_tags.png
+++ /dev/null
Binary files differ
diff --git a/doc/install/aws/img/create_route_table.png b/doc/install/aws/img/create_route_table.png
deleted file mode 100644
index ea72c57257e..00000000000
--- a/doc/install/aws/img/create_route_table.png
+++ /dev/null
Binary files differ
diff --git a/doc/install/azure/img/azure-vm-management-settings-network-interfaces.png b/doc/install/azure/img/azure-vm-management-settings-network-interfaces.png
deleted file mode 100644
index 4ff10718059..00000000000
--- a/doc/install/azure/img/azure-vm-management-settings-network-interfaces.png
+++ /dev/null
Binary files differ
diff --git a/doc/install/azure/img/azure-vm-management.png b/doc/install/azure/img/azure-vm-management.png
deleted file mode 100644
index a0e0067258c..00000000000
--- a/doc/install/azure/img/azure-vm-management.png
+++ /dev/null
Binary files differ
diff --git a/doc/install/installation.md b/doc/install/installation.md
index f16bc04af34..6b24dc8abdc 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -99,7 +99,20 @@ sudo apt-get install -y git-core
git --version
```
-Is the system packaged Git too old? Remove it and compile from source.
+Starting with GitLab 12.0, Git is required to be compiled with `libpcre2`.
+Find out if that's the case:
+
+```sh
+ldd /usr/local/bin/git | grep pcre2
+```
+
+The output should be similar to:
+
+```
+libpcre2-8.so.0 => /usr/lib/libpcre2-8.so.0 (0x00007f08461c3000)
+```
+
+Is the system packaged Git too old, or not compiled with pcre2? Remove it and compile from source:
```sh
# Remove packaged Git
@@ -108,12 +121,21 @@ sudo apt-get remove git-core
# Install dependencies
sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libssl-dev build-essential
+# Download and compile pcre2 from source
+curl --silent --show-error --location https://ftp.pcre.org/pub/pcre/pcre2-10.33.tar.gz --output pcre2.tar.gz
+tar -xzf pcre2.tar.gz
+cd pcre2-10.33
+chmod +x configure
+./configure --prefix=/usr --enable-jit
+make
+make install
+
# Download and compile from source
cd /tmp
curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.21.0.tar.gz
echo '85eca51c7404da75e353eba587f87fea9481ba41e162206a6f70ad8118147bee git-2.21.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.21.0.tar.gz
cd git-2.21.0/
-./configure
+./configure --with-libpcre
make prefix=/usr/local all
# Install into /usr/local/bin
@@ -434,7 +456,8 @@ sudo -u git -H editor config/resque.yml
```
CAUTION: **Caution:**
-Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup.
+Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup.
+If you want to use Puma web server, see [Using Puma](#using-puma) for the additional steps.
NOTE: **Note:**
If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps.
@@ -448,6 +471,18 @@ sudo -u git cp config/database.yml.postgresql config/database.yml
# MySQL only:
sudo -u git cp config/database.yml.mysql config/database.yml
+# PostgreSQL only:
+# Remove host, username, and password lines from config/database.yml.
+# Once modified, the `production` settings will be as follows:
+#
+# production:
+# adapter: postgresql
+# encoding: unicode
+# database: gitlabhq_production
+# pool: 10
+#
+sudo -u git -H editor config/database.yml
+
# MySQL and remote PostgreSQL only:
# Update username/password in config/database.yml.
# You only need to adapt the production settings (first part).
@@ -565,6 +600,18 @@ sudo -u git -H editor config.toml
For more information about configuring Gitaly see
[doc/administration/gitaly](../administration/gitaly).
+### Start Gitaly
+
+Gitaly must be running for the next section.
+
+```sh
+gitlab_path=/home/git/gitlab
+gitaly_path=/home/git/gitaly
+
+sudo -u git -H $gitlab_path/bin/daemon_with_pidfile $gitlab_path/tmp/pids/gitaly.pid \
+ $gitaly_path/gitaly $gitaly_path/config.toml >> $gitlab_path/log/gitaly.log 2>&1 &
+```
+
### Initialize Database and Activate Advanced Features
```sh
@@ -640,6 +687,12 @@ sudo -u git -H yarn install --production --pure-lockfile
sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
```
+If `rake` fails with `JavaScript heap out of memory` error, try to run it with `NODE_OPTIONS` set as follows.
+
+```sh
+sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production NODE_OPTIONS="--max_old_space_size=4096"
+```
+
### Start Your GitLab Instance
```sh
@@ -845,6 +898,25 @@ You also need to change the corresponding options (e.g. `ssh_user`, `ssh_host`,
Apart from the always supported markdown style, there are other rich text files that GitLab can display. But you might have to install a dependency to do so. See the [github-markup gem README](https://github.com/gitlabhq/markup#markups) for more information.
+### Using Puma
+
+Puma is a multi-threaded HTTP 1.1 server for Ruby applications.
+
+To use GitLab with Puma:
+
+1. Finish GitLab setup so you have it up and running.
+1. Copy the supplied example Puma config file into place:
+
+ ```sh
+ cd /home/git/gitlab
+
+ # Copy config file for the web server
+ sudo -u git -H config/puma.rb.example config/puma.rb
+ ```
+
+1. Edit the system `init.d` script to use `EXPERIMENTAL_PUMA=1` flag. If you have `/etc/default/gitlab`, then you should edit it instead.
+1. Restart GitLab.
+
## Troubleshooting
### "You appear to have cloned an empty repository."
diff --git a/doc/install/ldap.md b/doc/install/ldap.md
index a19f0342b65..d8d54864586 100644
--- a/doc/install/ldap.md
+++ b/doc/install/ldap.md
@@ -2,6 +2,4 @@
redirect_to: '../administration/auth/ldap.md'
---
-# GitLab LDAP integration
-
-This document was moved under [`administration/auth/ldap`](../administration/auth/ldap.md).
+This document was moved to [another location](../administration/auth/ldap.md).
diff --git a/doc/install/openshift_and_gitlab/img/pods-overview.png b/doc/install/openshift_and_gitlab/img/pods-overview.png
deleted file mode 100644
index 65927f65f4f..00000000000
--- a/doc/install/openshift_and_gitlab/img/pods-overview.png
+++ /dev/null
Binary files differ
diff --git a/doc/install/openshift_and_gitlab/img/storage-volumes.png b/doc/install/openshift_and_gitlab/img/storage-volumes.png
deleted file mode 100644
index 3fd092919bb..00000000000
--- a/doc/install/openshift_and_gitlab/img/storage-volumes.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index 7cef664bc98..d1d12dfd064 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -85,7 +85,7 @@ To build and install the indexer, run:
```sh
git clone https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer.git
-cd /gitlab-elasticsearch-indexer
+cd gitlab-elasticsearch-indexer
make
sudo make install
```
@@ -131,6 +131,8 @@ The following Elasticsearch settings are available:
| `Use the new repository indexer (beta)` | Perform repository indexing using [GitLab Elasticsearch Indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). |
| `Search with Elasticsearch enabled` | Enables/disables using Elasticsearch in search. |
| `URL` | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., "http://host1, https://host2:9200"). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (e.g., `http://<username>:<password>@<elastic_host>:9200/`). |
+| `Number of Elasticsearch shards` | Elasticsearch indexes are split into multiple shards for performance reasons. In general, larger indexes need to have more shards. Changes to this value do not take effect until the index is recreated. You can read more about tradeoffs in the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html#create-index-settings) |
+| `Number of Elasticsearch replicas` | Each Elasticsearch shard can have a number of replicas. These are a complete copy of the shard, and can provide increased query performance or resilience against hardware failure. Increasing this value will greatly increase total disk space required by the index. |
| `Limit namespaces and projects that can be indexed` | Enabling this will allow you to select namespaces and projects to index. All other namespaces and projects will use database search instead. Please note that if you enable this option but do not select any namespaces or projects, none will be indexed. [Read more below](#limiting-namespaces-and-projects).
| `Using AWS hosted Elasticsearch with IAM credentials` | Sign your Elasticsearch requests using [AWS IAM authorization](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) or [AWS EC2 Instance Profile Credentials](http://docs.aws.amazon.com/codedeploy/latest/userguide/getting-started-create-iam-instance-profile.html#getting-started-create-iam-instance-profile-cli). The policies must be configured to allow `es:*` actions. |
| `AWS Region` | The AWS region your Elasticsearch service is located in. |
diff --git a/doc/integration/img/google_app.png b/doc/integration/img/google_app.png
deleted file mode 100644
index 08f230452b4..00000000000
--- a/doc/integration/img/google_app.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/salesforce.md b/doc/integration/salesforce.md
index 18d42486fd6..8a99641a256 100644
--- a/doc/integration/salesforce.md
+++ b/doc/integration/salesforce.md
@@ -1,15 +1,15 @@
# SalesForce OmniAuth Provider
-You can integrate your GitLab instance with [SalesForce](https://www.salesforce.com/) to enable users to login to your GitLab instance with their SalesForce account.
+You can integrate your GitLab instance with [SalesForce](https://www.salesforce.com/) to enable users to login to your GitLab instance with their SalesForce account.
## Create SalesForce Application
To enable SalesForce OmniAuth provider, you must use SalesForce's credentials for your GitLab instance.
-To get the credentials (a pair of Client ID and Client Secret), you must register an application on UltraAuth.
+To get the credentials (a pair of Client ID and Client Secret), you must register an application on SalesForces.
1. Sign in to [SalesForce](https://www.salesforce.com/).
-1. Navigate to **Platform Tools/Apps** and click on **New Connected App**.
+1. Navigate to **Platform Tools/Apps/App Manager** and click on **New Connected App**.
1. Fill in the application details into the following fields:
- **Connected App Name** and **API Name**: Set to any value but consider something like `<Organization>'s GitLab`, `<Your Name>'s GitLab`, or something else that is descriptive.
@@ -64,7 +64,7 @@ To get the credentials (a pair of Client ID and Client Secret), you must registe
}
```
1. Change `SALESFORCE_CLIENT_ID` to the Consumer Key from the SalesForce connected application page.
-1. Change `SALESFORCE_CLIENT_SECRET` to the Client Secret from the SalesForce connected application page.
+1. Change `SALESFORCE_CLIENT_SECRET` to the Consumer Secret from the SalesForce connected application page.
![SalesForce App Secret Details](img/salesforce_app_secret_details.png)
1. Save the configuration file.
diff --git a/doc/license/README.md b/doc/license/README.md
index 4cc387ba95f..b9281fd5299 100644
--- a/doc/license/README.md
+++ b/doc/license/README.md
@@ -2,4 +2,4 @@
redirect_to: 'https://docs.gitlab.com/ee/user/admin_area/license.html'
---
-This document was moved to [user/admin_area/license](https://docs.gitlab.com/ee/user/admin_area/license.html).
+This document was moved to [another location](https://docs.gitlab.com/ee/user/admin_area/license.html).
diff --git a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
deleted file mode 100644
index 51eef90068d..00000000000
--- a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
+++ /dev/null
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_dashboard_import.png b/doc/monitoring/performance/img/grafana_dashboard_import.png
deleted file mode 100644
index fd639ee0eb8..00000000000
--- a/doc/monitoring/performance/img/grafana_dashboard_import.png
+++ /dev/null
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_configuration.png b/doc/monitoring/performance/img/grafana_data_source_configuration.png
deleted file mode 100644
index a98e0ed1e7d..00000000000
--- a/doc/monitoring/performance/img/grafana_data_source_configuration.png
+++ /dev/null
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_empty.png b/doc/monitoring/performance/img/grafana_data_source_empty.png
deleted file mode 100644
index 549ada8343e..00000000000
--- a/doc/monitoring/performance/img/grafana_data_source_empty.png
+++ /dev/null
Binary files differ
diff --git a/doc/monitoring/performance/img/grafana_save_icon.png b/doc/monitoring/performance/img/grafana_save_icon.png
deleted file mode 100644
index 68a071f5ae2..00000000000
--- a/doc/monitoring/performance/img/grafana_save_icon.png
+++ /dev/null
Binary files differ
diff --git a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png b/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png
deleted file mode 100644
index b9563a00e97..00000000000
--- a/doc/monitoring/performance/img/metrics_gitlab_configuration_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 0531627b7b3..00b6c1dfdc2 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -17,11 +17,11 @@ This page gathers all the resources for the topic **Authentication** within GitL
## GitLab administrators
- [LDAP (Community Edition)](../../administration/auth/ldap.md)
-- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html)
+- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html) **[STARTER]**
- [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.html)
+ - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html) **[STARTER]**
- [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:**
@@ -30,10 +30,10 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Atlassian Crowd OmniAuth Provider](../../administration/auth/crowd.md)
- [CAS OmniAuth Provider](../../integration/cas.md)
- [SAML OmniAuth Provider](../../integration/saml.md)
- - [SAML for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/index.html)
- - [SCIM user provisioning for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/scim_setup.html)
+ - [SAML for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/index.html) **[SILVER ONLY]**
+ - [SCIM user provisioning for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/scim_setup.html) **[SILVER ONLY]**
- [Okta SSO provider](../../administration/auth/okta.md)
- - [Kerberos integration (GitLab EE)](https://docs.gitlab.com/ee/integration/kerberos.html)
+ - [Kerberos integration (GitLab EE)](https://docs.gitlab.com/ee/integration/kerberos.html) **[STARTER]**
## API
diff --git a/doc/topics/autodevops/img/autodevops_domain_variables.png b/doc/topics/autodevops/img/autodevops_domain_variables.png
deleted file mode 100644
index b6f8864796f..00000000000
--- a/doc/topics/autodevops/img/autodevops_domain_variables.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_connect_cluster.png b/doc/topics/autodevops/img/guide_connect_cluster.png
deleted file mode 100644
index 703d536f37a..00000000000
--- a/doc/topics/autodevops/img/guide_connect_cluster.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_create_cluster.png b/doc/topics/autodevops/img/guide_create_cluster.png
deleted file mode 100644
index cd1d0fdd8da..00000000000
--- a/doc/topics/autodevops/img/guide_create_cluster.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_gke_apis_after.png b/doc/topics/autodevops/img/guide_gke_apis_after.png
deleted file mode 100644
index 380de958867..00000000000
--- a/doc/topics/autodevops/img/guide_gke_apis_after.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_gke_apis_before.png b/doc/topics/autodevops/img/guide_gke_apis_before.png
deleted file mode 100644
index d06fc707887..00000000000
--- a/doc/topics/autodevops/img/guide_gke_apis_before.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_merge_request_ide.png b/doc/topics/autodevops/img/guide_merge_request_ide.png
deleted file mode 100644
index c825b0849e1..00000000000
--- a/doc/topics/autodevops/img/guide_merge_request_ide.png
+++ /dev/null
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 2884458a44c..5a8744d71f9 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -771,6 +771,7 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `K8S_SECRET_*` | From GitLab 11.7, any variable prefixed with [`K8S_SECRET_`](#application-secret-variables) will be made available by Auto DevOps as environment variables to the deployed application. |
| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/index.md#base-domain) for more information. |
| `ROLLOUT_RESOURCE_TYPE` | From GitLab 11.9, this variable allows specification of the resource type being deployed when using a custom helm chart. Default value is `deployment`. |
+| `ROLLOUT_STATUS_DISABLED` | From GitLab 12.0, this variable allows to disable rollout status check because it doesn't support all resource types, for example, `cronjob`. |
| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, this variable allows extra arguments in `helm` commands when deploying the application. Note that using quotes will not prevent word splitting. |
TIP: **Tip:**
@@ -1099,3 +1100,7 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/
[ee]: https://about.gitlab.com/pricing/
[ce-21955]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21955
[ce-19507]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19507
+
+## Development guides
+
+Configuring [GDK for Auto DevOps](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/auto_devops.md).
diff --git a/doc/university/high-availability/aws/img/elastic-file-system.png b/doc/university/high-availability/aws/img/elastic-file-system.png
deleted file mode 100644
index 5bcfb8d0588..00000000000
--- a/doc/university/high-availability/aws/img/elastic-file-system.png
+++ /dev/null
Binary files differ
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index f90bff662e2..c2dff21b028 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -67,7 +67,7 @@ sudo -u git -H bundle exec rake gettext:pack RAILS_ENV=production
sudo -u git -H bundle exec rake gettext:po_to_json RAILS_ENV=production
# Clean up assets and cache
-sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production NODE_ENV=production
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production NODE_ENV=production NODE_OPTIONS="--max_old_space_size=4096"
```
### 4. Update gitlab-workhorse to the corresponding version
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index 5118726cb0a..82a86f3f343 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -25,13 +25,7 @@ This section contains all the steps necessary to upgrade Community Edition or
Enterprise Edition, regardless of the version you are upgrading to. Version
specific guidelines (should there be any) are covered separately.
-### 1. Stop server
-
-```bash
-sudo service gitlab stop
-```
-
-### 2. Backup
+### 1. Backup
NOTE: If you installed GitLab from source, make sure `rsync` is installed.
@@ -41,6 +35,12 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
+### 2. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
### 3. Update Ruby
NOTE: Beginning in GitLab 11.6, we only support Ruby 2.5 or higher, and dropped
@@ -314,7 +314,7 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
# Update node dependencies and recompile assets
-sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production NODE_OPTIONS="--max_old_space_size=4096"
# Clean up cache
sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
diff --git a/doc/user/admin_area/img/license_no_license_message.png b/doc/user/admin_area/img/license_no_license_message.png
deleted file mode 100644
index 87b397f7905..00000000000
--- a/doc/user/admin_area/img/license_no_license_message.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 42fc4efa4ce..9dd476656ed 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -118,7 +118,15 @@ will be synced to your Group and you can visualize it from the
![Additional minutes](img/additional_minutes.png)
-NOTE: **Important note**: If you have some minutes used over your default quota, these minutes will
+Be aware that:
+
+1. If you have purchased extra CI minutes before the purchase of a paid plan,
+we will calculate a pro-rated charge for your paid plan. That means you may
+be charged for less than one year since your subscription was previously
+created with the extra CI minutes.
+1. Once the extra CI minutes has been assigned to a Group they cannot be transferred
+to a different Group.
+1. If you have some minutes used over your default quota, these minutes will
be deducted from your Additional Minutes quota immediately after your purchase of additional
minutes.
diff --git a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png b/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png
deleted file mode 100644
index 723be23e77b..00000000000
--- a/doc/user/admin_area/settings/img/admin_area_default_artifacts_expiration.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/settings/img/admin_area_group_edit.png b/doc/user/admin_area/settings/img/admin_area_group_edit.png
deleted file mode 100644
index c9bd2f10b36..00000000000
--- a/doc/user/admin_area/settings/img/admin_area_group_edit.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/settings/img/admin_area_groups.png b/doc/user/admin_area/settings/img/admin_area_groups.png
deleted file mode 100644
index ebdee0eafdc..00000000000
--- a/doc/user/admin_area/settings/img/admin_area_groups.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png b/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
deleted file mode 100644
index 3f827f1f7a3..00000000000
--- a/doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.png b/doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.png
deleted file mode 100644
index 269a3cf1fbc..00000000000
--- a/doc/user/admin_area/settings/img/ci_shared_runners_build_minutes_quota.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/settings/img/group_quota_view.png b/doc/user/admin_area/settings/img/group_quota_view.png
deleted file mode 100644
index 791bfd868e0..00000000000
--- a/doc/user/admin_area/settings/img/group_quota_view.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/settings/img/group_settings.png b/doc/user/admin_area/settings/img/group_settings.png
deleted file mode 100644
index a849d9cfdc1..00000000000
--- a/doc/user/admin_area/settings/img/group_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index ce86ade3c1a..5c635b09503 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -122,12 +122,13 @@ container_scanning:
## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
CI_APPLICATION_TAG: $CI_COMMIT_SHA
+ CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1
allow_failure: true
services:
- docker:stable-dind
script:
- docker run -d --name db arminc/clair-db:latest
- - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.6
+ - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:${CLAIR_LOCAL_SCAN_VERSION}
- apk add -U wget ca-certificates
- docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
- wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
@@ -164,12 +165,13 @@ container_scanning:
## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
CI_APPLICATION_TAG: $CI_COMMIT_SHA
+ CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1
allow_failure: true
services:
- docker:stable-dind
script:
- docker run -d --name db arminc/clair-db:latest
- - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:v2.0.6
+ - docker run -p 6060:6060 --link db:postgres -d --name clair --restart on-failure arminc/clair-local-scan:${CLAIR_LOCAL_SCAN_VERSION}
- apk add -U wget ca-certificates
- docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
- wget https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 904c9e8fefe..f3b7d7fd471 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -138,7 +138,7 @@ variables:
#### Customizing the DAST settings
-The SAST settings can be changed through environment variables by using the
+The DAST settings can be changed through environment variables by using the
[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`.
These variables are documented in the [DAST README](https://gitlab.com/gitlab-org/security-products/dast#settings).
diff --git a/doc/user/group/custom_project_templates.md b/doc/user/group/custom_project_templates.md
index 8e101407ac0..f67325272a6 100644
--- a/doc/user/group/custom_project_templates.md
+++ b/doc/user/group/custom_project_templates.md
@@ -1,4 +1,4 @@
-# Custom group-level project templates **[PREMIUM ONLY]**
+# Custom group-level project templates **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing) 11.6.
diff --git a/doc/user/group/dependency_proxy/img/group_dependency_proxy.png b/doc/user/group/dependency_proxy/img/group_dependency_proxy.png
new file mode 100644
index 00000000000..035aff0b6c4
--- /dev/null
+++ b/doc/user/group/dependency_proxy/img/group_dependency_proxy.png
Binary files differ
diff --git a/doc/user/group/dependency_proxy/index.md b/doc/user/group/dependency_proxy/index.md
new file mode 100644
index 00000000000..4fc2d8e9509
--- /dev/null
+++ b/doc/user/group/dependency_proxy/index.md
@@ -0,0 +1,74 @@
+# Dependency Proxy **[PREMIUM]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
+
+NOTE: **Note:**
+This is the user guide. In order to use the dependency proxy, an administrator
+must first [configure it](../../../administration/dependency_proxy.md).
+
+For many organizations, it is desirable to have a local proxy for frequently used
+upstream images/packages. In the case of CI/CD, the proxy is responsible for
+receiving a request and returning the upstream image from a registry, acting
+as a pull-through cache.
+
+The dependency proxy is available in the group level. To access it, navigate to
+a group's **Overview > Dependency Proxy**.
+
+![Dependency Proxy group page](img/group_dependency_proxy.png)
+
+## Supported dependency proxies
+
+NOTE: **Note:**
+For a list of the upcoming additions to the proxies, visit the
+[direction page](https://about.gitlab.com/direction/package/dependency_proxy/#top-vision-items).
+
+The following dependency proxies are supported.
+
+| Dependency proxy | GitLab version |
+| ---------------- | -------------- |
+| Docker | 11.11+ |
+
+## Using the Docker dependency proxy
+
+With the Docker dependency proxy, you can use GitLab as a source for a Docker image.
+To get a Docker image into the dependency proxy:
+
+1. Find the proxy URL on your group's page under **Overview > Dependency Proxy**,
+ for example `gitlab.com/groupname/dependency_proxy/containers`.
+1. Trigger GitLab to pull the Docker image you want (e.g., `alpine:latest` or
+ `linuxserver/nextcloud:latest`) and store it in the proxy storage by using
+ one of the following ways:
+
+ - Manually pulling the Docker image:
+
+ ```bash
+ docker pull gitlab.com/groupname/dependency_proxy/containers/alpine:latest
+ ```
+
+ - From a `Dockerfile`:
+
+ ```bash
+ FROM gitlab.com/groupname/dependency_proxy/containers/alpine:latest
+ ```
+
+ - In [`.gitlab-ci.yml`](../../../ci/yaml/README.md#image):
+
+ ```bash
+ image: gitlab.com/groupname/dependency_proxy/containers/alpine:latest
+ ```
+
+GitLab will then pull the Docker image from Docker Hub and will cache the blobs
+on the GitLab server. The next time you pull the same image, it will get the latest
+information about the image from Docker Hub but will serve the existing blobs
+from GitLab.
+
+The blobs are kept forever, and there is no hard limit on how much data can be
+stored.
+
+## Limitations
+
+The following limitations apply:
+
+- Only public groups are supported (authentication is not supported yet).
+- Only Docker Hub is supported.
+- This feature requires Docker Hub being available.
diff --git a/doc/user/group/img/group_issue_board.png b/doc/user/group/img/group_issue_board.png
deleted file mode 100644
index a0da74a320f..00000000000
--- a/doc/user/group/img/group_issue_board.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/img/new_group_form.png b/doc/user/group/img/new_group_form.png
deleted file mode 100644
index 1c4d3ec6ceb..00000000000
--- a/doc/user/group/img/new_group_form.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 06564fd6cd1..a5e3bfda70e 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -368,5 +368,9 @@ and issues) performed by your group members.
With [GitLab Issues Analytics](issues_analytics/index.md), in groups, you can see a bar chart of the number of issues created each month.
+## Dependency Proxy **[PREMIUM]**
+
+Use GitLab as a [dependency proxy](dependency_proxy/index.md) for upstream Docker images.
+
[ee]: https://about.gitlab.com/pricing/
[ee-2534]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2534
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index ee3137d032e..53116606201 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -22,8 +22,16 @@ SAML SSO for groups is used only as a convenient way to add users and does not s
![Issuer and callback for configuring SAML identity provider with GitLab.com](img/group_saml_configuration_information.png)
-NOTE: **Note:**
-Partial SSO enforcement was introduced in [11.8](https://gitlab.com/gitlab-org/gitlab-ee/issues/5291). With this option enabled, users must use your group's GitLab single sign on URL to be added to the group or be added via SCIM. Users can no longer be added manually. After a user has been added to the group, GitLab does not continue to enforce the use of SSO, but we'll [add a persistent check](https://gitlab.com/gitlab-org/gitlab-ee/issues/9255) in a later version.
+### SSO enforcement
+
+SSO enforcement was:
+
+- [Introduced in GitLab 11.8](https://gitlab.com/gitlab-org/gitlab-ee/issues/5291).
+- [Improved upon in GitLab 11.11 with ongoing enforcement in the GitLab UI](https://gitlab.com/gitlab-org/gitlab-ee/issues/9255).
+
+With this option enabled, users must use your group's GitLab single sign on URL to be added to the group or be added via SCIM. Users cannot be added manually, and may only access project/group resources via the UI by signing in through the SSO URL.
+
+We intend to add a similar SSO requirement for [Git and API activity](https://gitlab.com/gitlab-org/gitlab-ee/issues/9152) in the future.
### NameID
diff --git a/doc/user/img/mermaid_diagram_render_gfm.png b/doc/user/img/mermaid_diagram_render_gfm.png
deleted file mode 100644
index 9d192a30a85..00000000000
--- a/doc/user/img/mermaid_diagram_render_gfm.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index c1c2c036bae..5dad9621802 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -286,15 +286,15 @@ On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/he
Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
```
-Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/monkey.png" width="20px" height="20px" style="display:inline;margin:0"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/star2.png" width="20px" height="20px" style="display:inline;margin:0"> to your <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/speech_balloon.png" width="20px" height="20px" style="display:inline;margin:0">. Well we have a gift for you:
+Sometimes you want to <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/monkey.png" width="20px" height="20px" style="display:inline;margin:0"> around a bit and add some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/star2.png" width="20px" height="20px" style="display:inline;margin:0"> to your <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speech_balloon.png" width="20px" height="20px" style="display:inline;margin:0">. Well we have a gift for you:
-<img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/zap.png" width="20px" height="20px" style="display:inline;margin:0">You can use emoji anywhere GFM is supported. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/v.png" width="20px" height="20px" style="display:inline;margin:0">
+<img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/zap.png" width="20px" height="20px" style="display:inline;margin:0">You can use emoji anywhere GFM is supported. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/v.png" width="20px" height="20px" style="display:inline;margin:0">
-You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/bug.png" width="20px" height="20px" style="display:inline;margin:0"> or warn about <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/speak_no_evil.png" width="20px" height="20px" style="display:inline;margin:0"> patches. And if someone improves your really <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/snail.png" width="20px" height="20px" style="display:inline;margin:0"> code, send them some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/birthday.png" width="20px" height="20px" style="display:inline;margin:0">. People will <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/heart.png" width="20px" height="20px" style="display:inline;margin:0"> you for that.
+You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/bug.png" width="20px" height="20px" style="display:inline;margin:0"> or warn about <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/speak_no_evil.png" width="20px" height="20px" style="display:inline;margin:0"> patches. And if someone improves your really <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/snail.png" width="20px" height="20px" style="display:inline;margin:0"> code, send them some <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/birthday.png" width="20px" height="20px" style="display:inline;margin:0">. People will <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/heart.png" width="20px" height="20px" style="display:inline;margin:0"> you for that.
-If you are new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/fearful.png" width="20px" height="20px" style="display:inline;margin:0">. You can easily join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/family.png" width="20px" height="20px" style="display:inline;margin:0">. All you need to do is to look up one of the supported codes.
+If you are new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/fearful.png" width="20px" height="20px" style="display:inline;margin:0">. You can easily join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/family.png" width="20px" height="20px" style="display:inline;margin:0">. All you need to do is to look up one of the supported codes.
-Consult the [Emoji Cheat Sheet](https://www.webfx.com/tools/emoji-cheat-sheet/) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/public/-/emojis/1/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0">
+Consult the [Emoji Cheat Sheet](https://www.webfx.com/tools/emoji-cheat-sheet/) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0">
Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support.
@@ -480,7 +480,7 @@ GitLab 10.3.
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#mermaid
-It is possible to generate diagrams and flowcharts from text using [Mermaid][mermaid].
+It is possible to generate diagrams and flowcharts from text using [Mermaid](https://mermaidjs.github.io/).
In order to generate a diagram or flowchart, you should write your text inside the `mermaid` block.
@@ -496,9 +496,15 @@ Example:
Becomes:
-<img src="./img/mermaid_diagram_render_gfm.png" width="200px" height="400px">
+```mermaid
+graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+```
-For details see the [Mermaid official page][mermaid].
+For details see the [Mermaid official page](https://mermaidjs.github.io/).
### Front matter
@@ -1072,7 +1078,6 @@ A link starting with a `/` is relative to the wiki root.
[^2]: This is my awesome footnote.
[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
-[mermaid]: https://mermaidjs.github.io/ "Mermaid website"
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 9b298e4eb30..318053fdabb 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -11,7 +11,7 @@ project itself, the highest permission level is used.
On public and internal projects the Guest role is not enforced. All users will
be able to create issues, leave comments, and clone or download the project code.
-When a member leaves the team all the assigned [Issues](project/issues/index.md) and [Merge Requests](project/merge_requests/index.md)
+When a member leaves a team's project, all the assigned [Issues](project/issues/index.md) and [Merge Requests](project/merge_requests/index.md)
will be unassigned automatically.
GitLab [administrators](../administration/index.md) receive all permissions.
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 157afb3a78c..afad8f0fa9d 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -674,11 +674,6 @@ To remove the Kubernetes cluster integration from your project, simply click the
**Remove integration** button. You will then be able to follow the procedure
and add a Kubernetes cluster again.
-## View Kubernetes pod logs from GitLab **[ULTIMATE]**
-
-Learn how to easily
-[view the logs of running pods in connected Kubernetes clusters](kubernetes_pod_logs.md).
-
## What you can get with the Kubernetes integration
Here's what you can do with GitLab if you enable the Kubernetes integration.
@@ -701,6 +696,12 @@ the need to leave GitLab.
[Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html)
+### Pod logs **[ULTIMATE]**
+
+GitLab makes it easy to view the logs of running pods in connected Kubernetes clusters. By displaying the logs directly in GitLab, developers can avoid having to manage console tools or jump to a different interface.
+
+[Read more about Kubernetes pod logs](kubernetes_pod_logs.md)
+
### Kubernetes monitoring
Automatically detect and monitor Kubernetes metrics. Automatic monitoring of
diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md
index a4937e6cf6b..ae04616943f 100644
--- a/doc/user/project/code_owners.md
+++ b/doc/user/project/code_owners.md
@@ -43,7 +43,7 @@ Example `CODEOWNERS` file:
# app/ @commented-rule
-# We can specifiy a default match using wildcards:
+# We can specify a default match using wildcards:
* @default-codeowner
# Rules defined later in the file take precedence over the rules
diff --git a/doc/user/project/img/assignee_lists.png b/doc/user/project/img/assignee_lists.png
deleted file mode 100644
index f2660cd8f80..00000000000
--- a/doc/user/project/img/assignee_lists.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png
deleted file mode 100644
index b753593d212..00000000000
--- a/doc/user/project/img/issue_board.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/labels_sidebar_inline.png b/doc/user/project/img/labels_sidebar_inline.png
deleted file mode 100644
index 2186f14ea92..00000000000
--- a/doc/user/project/img/labels_sidebar_inline.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/priority_sort_order.png b/doc/user/project/img/priority_sort_order.png
deleted file mode 100644
index cd1dd8237c0..00000000000
--- a/doc/user/project/img/priority_sort_order.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/project_security_dashboard.png b/doc/user/project/img/project_security_dashboard.png
deleted file mode 100644
index 3294e59e943..00000000000
--- a/doc/user/project/img/project_security_dashboard.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/protected_branches_choose_branch.png b/doc/user/project/img/protected_branches_choose_branch.png
deleted file mode 100644
index c2848db9c96..00000000000
--- a/doc/user/project/img/protected_branches_choose_branch.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/img/protected_branches_error_ui.png b/doc/user/project/img/protected_branches_error_ui.png
deleted file mode 100644
index 62839e49d89..00000000000
--- a/doc/user/project/img/protected_branches_error_ui.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/import/img/import_projects_from_github_select_auth_method.png b/doc/user/project/import/img/import_projects_from_github_select_auth_method.png
deleted file mode 100644
index 90e6243aec0..00000000000
--- a/doc/user/project/import/img/import_projects_from_github_select_auth_method.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/img/jira_project_name.png b/doc/user/project/integrations/img/jira_project_name.png
deleted file mode 100644
index 981c7f7ca18..00000000000
--- a/doc/user/project/integrations/img/jira_project_name.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/img/jira_service.png b/doc/user/project/integrations/img/jira_service.png
deleted file mode 100644
index 0ae2fa28756..00000000000
--- a/doc/user/project/integrations/img/jira_service.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_yaml_deploy.png b/doc/user/project/integrations/img/prometheus_yaml_deploy.png
deleted file mode 100644
index 78dd178a077..00000000000
--- a/doc/user/project/integrations/img/prometheus_yaml_deploy.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 8e1603f9ec9..10eb3a4f3b7 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -321,8 +321,14 @@ X-Gitlab-Event: Issue Hook
"group_id": 41
}],
"changes": {
- "updated_by_id": [null, 1],
- "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"],
+ "updated_by_id": {
+ "previous": null,
+ "current": 1
+ },
+ "updated_at": {
+ "previous": "2017-09-15 16:50:55 UTC",
+ "current": "2017-09-15 16:52:00 UTC"
+ },
"labels": {
"previous": [{
"id": 206,
@@ -851,8 +857,14 @@ X-Gitlab-Event: Merge Request Hook
"group_id": 41
}],
"changes": {
- "updated_by_id": [null, 1],
- "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"],
+ "updated_by_id": {
+ "previous": null,
+ "current": 1
+ },
+ "updated_at": {
+ "previous": "2017-09-15 16:50:55 UTC",
+ "current":"2017-09-15 16:52:00 UTC"
+ },
"labels": {
"previous": [{
"id": 206,
diff --git a/doc/user/project/issues/csv_import.md b/doc/user/project/issues/csv_import.md
index 032e3a73ad0..b0b1cfcfdf7 100644
--- a/doc/user/project/issues/csv_import.md
+++ b/doc/user/project/issues/csv_import.md
@@ -1,17 +1,23 @@
-# Importing Issues from CSV
+# Importing issues from CSV
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23532) in GitLab 11.7.
-Issues can be imported to a project by uploading a CSV file. Supported fields are
-`title` and `description`.
+Issues can be imported to a project by uploading a CSV file with the columns
+`title` and `description`, in that order.
The user uploading the CSV file will be set as the author of the imported issues.
> **Note:** A permission level of `Developer` or higher is required to import issues.
+## Prepare for the import
+
+- Consider importing a test file containing only a few issues. There is no way to undo a large import without using the GitLab API.
+- Ensure your CSV file meets the [file format](#csv-file-format) requirements.
+
+## Import the file
+
To import issues:
-1. Ensure your CSV file meets the [file format](#csv-file-format) requirements.
1. Navigate to a project's Issues list page.
1. If existing issues are present, click the import icon at the top right, next to the **Edit issues** button.
1. For a project without any issues, click the button labeled **Import CSV** in the middle of the page.
@@ -20,11 +26,11 @@ To import issues:
The file is processed in the background and a notification email is sent
to you once the import is completed.
-## CSV File Format
+## CSV file format
### Header row
-CSV files must contain a header row beginning with at least two columns, `title` and `description`, in that order.
+CSV files must contain a header row where the first column header is `title` and the second is `description`.
If additional columns are present, they will be ignored.
### Column separator
@@ -53,7 +59,7 @@ The limit depends on the configuration value of Max Attachment Size for the GitL
For GitLab.com, it is set to 10 MB.
-## Sample Data
+## Sample data
```csv
title,description
diff --git a/doc/user/project/issues/img/group_issues_list_view.png b/doc/user/project/issues/img/group_issues_list_view.png
deleted file mode 100644
index c951a9e2dcd..00000000000
--- a/doc/user/project/issues/img/group_issues_list_view.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/issues/img/issue_template.png b/doc/user/project/issues/img/issue_template.png
deleted file mode 100644
index 6cb2c07d27e..00000000000
--- a/doc/user/project/issues/img/issue_template.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index ac91cd4ea98..8e9e9aa79cf 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -131,7 +131,7 @@ From the project issue list page and the project merge request list page, you ca
From the group issue list page and the group merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group labels (including subgroup ancestors and subgroup descendants) and project labels.
-From the group epic list page, you can [filter](../search/index.md#issues-and-merge-requests) by both current group labels as well as decendent group labels.
+From the group epic list page, you can [filter](../search/index.md#issues-and-merge-requests) by both current group labels as well as descendant group labels.
![Labels group issues](img/labels_group_issues.png)
diff --git a/doc/user/project/members/img/add_new_user_to_project_settings.png b/doc/user/project/members/img/add_new_user_to_project_settings.png
deleted file mode 100644
index e49ea1a3e3d..00000000000
--- a/doc/user/project/members/img/add_new_user_to_project_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/add_user_members_menu.png b/doc/user/project/members/img/add_user_members_menu.png
deleted file mode 100644
index 6f08088b52f..00000000000
--- a/doc/user/project/members/img/add_user_members_menu.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/members/img/max_access_level.png b/doc/user/project/members/img/max_access_level.png
deleted file mode 100644
index 42a0416ffbb..00000000000
--- a/doc/user/project/members/img/max_access_level.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/container_scanning.png b/doc/user/project/merge_requests/img/container_scanning.png
deleted file mode 100644
index e47f62acd9d..00000000000
--- a/doc/user/project/merge_requests/img/container_scanning.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/create-issue-with-list-hover.png b/doc/user/project/merge_requests/img/create-issue-with-list-hover.png
deleted file mode 100644
index 7d70e8299f5..00000000000
--- a/doc/user/project/merge_requests/img/create-issue-with-list-hover.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/dast_all.png b/doc/user/project/merge_requests/img/dast_all.png
deleted file mode 100644
index b6edc928dc3..00000000000
--- a/doc/user/project/merge_requests/img/dast_all.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/dast_single.png b/doc/user/project/merge_requests/img/dast_single.png
deleted file mode 100644
index 26ca4bde786..00000000000
--- a/doc/user/project/merge_requests/img/dast_single.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/dependency_scanning.png b/doc/user/project/merge_requests/img/dependency_scanning.png
deleted file mode 100644
index 18df356f846..00000000000
--- a/doc/user/project/merge_requests/img/dependency_scanning.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/interactive_reports.png b/doc/user/project/merge_requests/img/interactive_reports.png
deleted file mode 100644
index 9f9812dc69d..00000000000
--- a/doc/user/project/merge_requests/img/interactive_reports.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/license_management.png b/doc/user/project/merge_requests/img/license_management.png
deleted file mode 100644
index cdce6b5fe38..00000000000
--- a/doc/user/project/merge_requests/img/license_management.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/license_management_decision.png b/doc/user/project/merge_requests/img/license_management_decision.png
deleted file mode 100644
index 0763130c375..00000000000
--- a/doc/user/project/merge_requests/img/license_management_decision.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/license_management_pipeline_tab.png b/doc/user/project/merge_requests/img/license_management_pipeline_tab.png
deleted file mode 100644
index 80ffca815b9..00000000000
--- a/doc/user/project/merge_requests/img/license_management_pipeline_tab.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/license_management_settings.png b/doc/user/project/merge_requests/img/license_management_settings.png
deleted file mode 100644
index b5490e59074..00000000000
--- a/doc/user/project/merge_requests/img/license_management_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/revert_changes_commit.png b/doc/user/project/merge_requests/img/revert_changes_commit.png
deleted file mode 100644
index c9dd0019024..00000000000
--- a/doc/user/project/merge_requests/img/revert_changes_commit.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/revert_changes_mr.png b/doc/user/project/merge_requests/img/revert_changes_mr.png
deleted file mode 100644
index 06b841b3002..00000000000
--- a/doc/user/project/merge_requests/img/revert_changes_mr.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/sast.png b/doc/user/project/merge_requests/img/sast.png
deleted file mode 100644
index 2c75592c32a..00000000000
--- a/doc/user/project/merge_requests/img/sast.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/security_report.png b/doc/user/project/merge_requests/img/security_report.png
deleted file mode 100644
index ba41b707238..00000000000
--- a/doc/user/project/merge_requests/img/security_report.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/img/vulnerability_solution.png b/doc/user/project/merge_requests/img/vulnerability_solution.png
deleted file mode 100644
index 7443b9b6eea..00000000000
--- a/doc/user/project/merge_requests/img/vulnerability_solution.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index 265871a7b4b..d0291c4cef5 100644
--- a/doc/user/project/merge_requests/merge_request_approvals.md
+++ b/doc/user/project/merge_requests/merge_request_approvals.md
@@ -77,7 +77,7 @@ request approval rules:
1. Navigate to your project's **Settings > General** and expand **Merge request approvals**.
1. Click **Add approvers** to create a new approval rule.
-1. Just like in [GitLab Starter](#editing-approvals), select the approval members and aprovals required.
+1. Just like in [GitLab Starter](#editing-approvals), select the approval members and approvals required.
1. Give the approval rule a name that describes the set of approvers selected.
1. Click **Add approvers** to submit the new rule.
@@ -173,8 +173,7 @@ the merge request. To enable this feature:
1. Navigate to your project's **Settings > General** and expand
**Merge request approvals**.
-1. Tick the **Require approval from code owners** checkbox
- checkbox.
+1. Tick the **Require approval from code owners** checkbox.
1. Click **Save changes**.
When this feature is enabled, all merge requests will need approval
@@ -294,6 +293,18 @@ enabling [**Prevent approval of merge requests by their committers**](#prevent-a
1. Tick the checkbox **Prevent approval of merge requests by their committers**.
1. Click **Save changes**.
+## Require authentication when approving a merge request **[STARTER]**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5981) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.0.
+
+You can force the approver to enter a password in order to authenticate who is approving the merge request by
+enabling **Require user password to approve**. This enables an Electronic Signature
+for approvals such as the one defined by [CFR Part 11](https://www.accessdata.fda.gov/scripts/cdrh/cfdocs/cfcfr/CFRSearch.cfm?CFRPart=11&showFR=1&subpartNode=21:1.0.1.1.8.3)):
+
+1. Navigate to your project's **Settings > General** and expand **Merge request approvals**.
+1. Tick the checkbox **Require user password to approve**.
+1. Click **Save changes**.
+
## Merge requests with different source branch and target branch projects
If the merge request source branch and target branch belong to different
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 6c3fa5eb463..d36312c9b8d 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -44,14 +44,14 @@ It is important to note that we have a few types of users:
- **Administrators**: CI jobs created by Administrators will not have access
to all GitLab projects, but only to projects and container images of projects
- that the administrator is a member of.That means that if a project is either
+ that the administrator is a member of. That means that if a project is either
public or internal users have access anyway, but if a project is private, the
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](../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,
+ rules out accessing all internal projects by default.
This allows us to make the CI and permission system more trustworthy.
Let's consider the following scenario:
diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png
deleted file mode 100644
index 69e84b84984..00000000000
--- a/doc/user/project/pages/img/pages_create_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png
deleted file mode 100644
index 2f1a19ae424..00000000000
--- a/doc/user/project/pages/img/pages_create_user_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png
deleted file mode 100644
index 3e57f43f7ba..00000000000
--- a/doc/user/project/pages/img/pages_dns_details.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png
deleted file mode 100644
index 76c39101439..00000000000
--- a/doc/user/project/pages/img/pages_multiple_domains.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png
deleted file mode 100644
index cd59defa006..00000000000
--- a/doc/user/project/pages/img/pages_new_domain_button.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png
deleted file mode 100644
index 64e5f8eced1..00000000000
--- a/doc/user/project/pages/img/pages_upload_cert.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/web_ide/img/enable_web_ide.png b/doc/user/project/web_ide/img/enable_web_ide.png
deleted file mode 100644
index 196baa82ad2..00000000000
--- a/doc/user/project/web_ide/img/enable_web_ide.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index 95f606fd786..e3c6cd6d6ff 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -116,7 +116,9 @@ instructions.
## Customizing sidebar
-By default, the wiki would render a sidebar which lists all the pages for the
-wiki. You could as well provide a `_sidebar` page to replace this default
-sidebar. When this customized sidebar page is provided, the default sidebar
-would not be rendered, but the customized one.
+On the project's Wiki page, there is a right side navigation that renders the full Wiki pages list by default, with hierarchy.
+
+If the Wiki repository contains a `_sidebar` page, the file of this page replaces the default side navigation.
+This custom file serves to render it's custom content, fully replacing the standard sidebar.
+
+Support for displaying a generated TOC with a custom side navigation is planned.
diff --git a/doc/user/search/img/issues_any_assignee.png b/doc/user/search/img/issues_any_assignee.png
deleted file mode 100644
index 2f902bcc66c..00000000000
--- a/doc/user/search/img/issues_any_assignee.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/search/img/issues_author.png b/doc/user/search/img/issues_author.png
deleted file mode 100644
index 792f9746db6..00000000000
--- a/doc/user/search/img/issues_author.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/award_emoji.png b/doc/workflow/award_emoji.png
deleted file mode 100644
index 1ad634a343e..00000000000
--- a/doc/workflow/award_emoji.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/img/new_branch_from_issue.png b/doc/workflow/img/new_branch_from_issue.png
deleted file mode 100644
index 286d775bb9e..00000000000
--- a/doc/workflow/img/new_branch_from_issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index da0243705aa..202f2e39975 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -250,6 +250,7 @@ If you are storing LFS files outside of GitLab you can disable LFS on the projec
It is possible to host LFS objects externally by setting a custom LFS url with `git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs`.
-Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project.
+You might choose to do this if you are using an appliance like a Sonatype Nexus to store LFS data. If you choose to use an external LFS store,
+GitLab will not be able to verify LFS objects which means that pushes will fail if you have GitLab LFS support enabled.
-LFS can be disabled from the [Project settings](../../user/project/settings/index.md).
+To stop push failure, LFS support can be disabled in the [Project settings](../../user/project/settings/index.md). This means you will lose GitLab LFS value-adds (Verifying LFS objects, UI integration for LFS).
diff --git a/doc/workflow/share_with_group.png b/doc/workflow/share_with_group.png
deleted file mode 100644
index 2c47625e29a..00000000000
--- a/doc/workflow/share_with_group.png
+++ /dev/null
Binary files differ
diff --git a/fixtures/emojis/index.json b/fixtures/emojis/index.json
index f55571d31fa..0406d404532 100644
--- a/fixtures/emojis/index.json
+++ b/fixtures/emojis/index.json
@@ -16344,7 +16344,7 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "accomodation",
+ "accommodation",
"building",
"checkin",
"whotel",
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 90ed24a2ded..625fada4f08 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -542,10 +542,15 @@ module API
class IssueBasic < ProjectEntity
expose :closed_at
expose :closed_by, using: Entities::UserBasic
- expose :labels do |issue|
- # Avoids an N+1 query since labels are preloaded
- issue.labels.map(&:title).sort
+
+ expose :labels do |issue, options|
+ if options[:with_labels_details]
+ ::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
+ else
+ issue.labels.map(&:title).sort
+ end
end
+
expose :milestone, using: Entities::Milestone
expose :assignees, :author, using: Entities::UserBasic
@@ -573,6 +578,14 @@ module API
class Issue < IssueBasic
include ::API::Helpers::RelatedResourcesHelpers
+ expose(:has_tasks) do |issue, _|
+ !issue.task_list_items.empty?
+ end
+
+ expose :task_status, if: -> (issue, _) do
+ !issue.task_list_items.empty?
+ end
+
expose :_links do
expose :self do |issue|
expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid))
@@ -878,7 +891,7 @@ module API
expose :push_event_payload,
as: :push_data,
using: PushEventPayload,
- if: -> (event, _) { event.push? }
+ if: -> (event, _) { event.push_action? }
expose :author_username do |event, options|
event.author&.username
diff --git a/lib/api/helpers/issues_helpers.rb b/lib/api/helpers/issues_helpers.rb
index f6762910b0c..fc66cec5341 100644
--- a/lib/api/helpers/issues_helpers.rb
+++ b/lib/api/helpers/issues_helpers.rb
@@ -18,6 +18,39 @@ module API
:title
]
end
+
+ def issue_finder(args = {})
+ args = declared_params.merge(args)
+
+ args.delete(:id)
+ args[:milestone_title] ||= args.delete(:milestone)
+ args[:label_name] ||= args.delete(:labels)
+ args[:scope] = args[:scope].underscore if args[:scope]
+
+ IssuesFinder.new(current_user, args)
+ end
+
+ def find_issues(args = {})
+ finder = issue_finder(args)
+ issues = finder.execute.with_api_entity_associations
+
+ issues.reorder(order_options_with_tie_breaker) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def issues_statistics(args = {})
+ finder = issue_finder(args)
+ counter = Gitlab::IssuablesCountForState.new(finder)
+
+ {
+ statistics: {
+ counts: {
+ all: counter[:all],
+ closed: counter[:closed],
+ opened: counter[:opened]
+ }
+ }
+ }
+ end
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index d0a93b77951..0b4da01f3c8 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -3,27 +3,12 @@
module API
class Issues < Grape::API
include PaginationParams
+ helpers Helpers::IssuesHelpers
+ helpers ::Gitlab::IssuableMetadata
before { authenticate_non_get! }
- helpers ::Gitlab::IssuableMetadata
-
helpers do
- # rubocop: disable CodeReuse/ActiveRecord
- def find_issues(args = {})
- args = declared_params.merge(args)
-
- args.delete(:id)
- args[:milestone_title] = args.delete(:milestone)
- args[:label_name] = args.delete(:labels)
- args[:scope] = args[:scope].underscore if args[:scope]
-
- issues = IssuesFinder.new(current_user, args).execute
- .with_api_entity_associations
- issues.reorder(order_options_with_tie_breaker)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
if Gitlab.ee?
params :issues_params_ee do
optional :weight, types: [Integer, String], integer_none_any: true, desc: 'The weight of the issue'
@@ -34,13 +19,9 @@ module API
end
end
- params :issues_params do
+ params :issues_stats_params do
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :milestone, type: String, desc: 'Milestone title'
- optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
- desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
- optional :sort, type: String, values: %w[asc desc], default: 'desc',
- desc: 'Return issues sorted in `asc` or `desc` order.'
optional :milestone, type: String, desc: 'Return issues for a specific milestone'
optional :iids, type: Array[Integer], desc: 'The IID array of issues'
optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these'
@@ -49,18 +30,39 @@ module API
optional :created_before, type: DateTime, desc: 'Return issues created before the specified time'
optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time'
optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time'
+
optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID'
+ optional :author_username, type: String, desc: 'Return issues which are authored by the user with the given username'
+ mutually_exclusive :author_id, :author_username
+
optional :assignee_id, types: [Integer, String], integer_none_any: true,
desc: 'Return issues which are assigned to the user with the given ID'
+ optional :assignee_username, type: Array[String], check_assignees_count: true,
+ coerce_with: Validations::CheckAssigneesCount.coerce,
+ desc: 'Return issues which are assigned to the user with the given username'
+ mutually_exclusive :assignee_id, :assignee_username
+
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
optional :confidential, type: Boolean, desc: 'Filter confidential or public issues'
- use :pagination
use :issues_params_ee if Gitlab.ee?
end
+ params :issues_params do
+ optional :with_labels_details, type: Boolean, desc: 'Return more label data than just lable title', default: false
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
+ desc: 'Return opened, closed, or all issues'
+ optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
+ desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return issues sorted in `asc` or `desc` order.'
+
+ use :issues_stats_params
+ use :pagination
+ end
+
params :issue_params do
optional :description, type: String, desc: 'The description of an issue'
optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
@@ -75,13 +77,23 @@ module API
end
end
+ desc "Get currently authenticated user's issues statistics"
+ params do
+ use :issues_stats_params
+ optional :scope, type: String, values: %w[created_by_me assigned_to_me all], default: 'created_by_me',
+ desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
+ end
+ get '/issues_statistics' do
+ authenticate! unless params[:scope] == 'all'
+
+ present issues_statistics, with: Grape::Presenters::Presenter
+ end
+
resource :issues do
desc "Get currently authenticated user's issues" do
- success Entities::IssueBasic
+ success Entities::Issue
end
params do
- optional :state, type: String, values: %w[opened closed all], default: 'all',
- desc: 'Return opened, closed, or all issues'
use :issues_params
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me',
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
@@ -91,7 +103,8 @@ module API
issues = paginate(find_issues)
options = {
- with: Entities::IssueBasic,
+ with: Entities::Issue,
+ with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
@@ -105,11 +118,9 @@ module API
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group issues' do
- success Entities::IssueBasic
+ success Entities::Issue
end
params do
- optional :state, type: String, values: %w[opened closed all], default: 'all',
- desc: 'Return opened, closed, or all issues'
use :issues_params
end
get ":id/issues" do
@@ -118,13 +129,24 @@ module API
issues = paginate(find_issues(group_id: group.id, include_subgroups: true))
options = {
- with: Entities::IssueBasic,
+ with: Entities::Issue,
+ with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
present issues, options
end
+
+ desc 'Get statistics for the list of group issues'
+ params do
+ use :issues_stats_params
+ end
+ get ":id/issues_statistics" do
+ group = find_group!(params[:id])
+
+ present issues_statistics(group_id: group.id, include_subgroups: true), with: Grape::Presenters::Presenter
+ end
end
params do
@@ -134,11 +156,9 @@ module API
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
- success Entities::IssueBasic
+ success Entities::Issue
end
params do
- optional :state, type: String, values: %w[opened closed all], default: 'all',
- desc: 'Return opened, closed, or all issues'
use :issues_params
end
get ":id/issues" do
@@ -147,7 +167,8 @@ module API
issues = paginate(find_issues(project_id: project.id))
options = {
- with: Entities::IssueBasic,
+ with: Entities::Issue,
+ with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
project: user_project,
issuable_metadata: issuable_meta_data(issues, 'Issue')
@@ -156,6 +177,16 @@ module API
present issues, options
end
+ desc 'Get statistics for the list of project issues'
+ params do
+ use :issues_stats_params
+ end
+ get ":id/issues_statistics" do
+ project = find_project!(params[:id])
+
+ present issues_statistics(project_id: project.id), with: Grape::Presenters::Presenter
+ end
+
desc 'Get a single project issue' do
success Entities::Issue
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index ea36c24eca2..fdf4904e9f5 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -98,6 +98,7 @@ module API
optional :certificate, type: String, desc: %q(Session's certificate)
optional :authorization, type: String, desc: %q(Session's authorization)
end
+ optional :job_age, type: Integer, desc: %q(Job should be older than passed age in seconds to be ran on runner)
end
post '/request' do
authenticate_runner!
diff --git a/lib/api/search.rb b/lib/api/search.rb
index 60095300ea1..1cab1a97186 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -112,12 +112,13 @@ module API
type: String,
desc: 'The scope of the search',
values: Helpers::SearchHelpers.project_search_scopes
+ optional :ref, type: String, desc: 'The name of a repository branch or tag. If not given, the default branch is used'
use :pagination
end
get ':id/(-/)search' do
check_users_search_allowed!
- present search(project_id: user_project.id), with: entity
+ present search({ project_id: user_project.id, repository_ref: params[:ref] }), with: entity
end
end
end
diff --git a/lib/api/validations/check_assignees_count.rb b/lib/api/validations/check_assignees_count.rb
new file mode 100644
index 00000000000..836ec936b31
--- /dev/null
+++ b/lib/api/validations/check_assignees_count.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module API
+ module Validations
+ class CheckAssigneesCount < Grape::Validations::Base
+ def self.coerce
+ lambda do |value|
+ case value
+ when String, Array
+ Array.wrap(value)
+ else
+ []
+ end
+ end
+ end
+
+ def validate_param!(attr_name, params)
+ return if param_allowed?(attr_name, params)
+
+ raise Grape::Exceptions::Validation,
+ params: [@scope.full_name(attr_name)],
+ message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}"
+ end
+
+ private
+
+ def param_allowed?(attr_name, params)
+ params[attr_name].size <= 1
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/repo.rb b/lib/bitbucket_server/representation/repo.rb
index 6c494b79166..dab7f8f22a1 100644
--- a/lib/bitbucket_server/representation/repo.rb
+++ b/lib/bitbucket_server/representation/repo.rb
@@ -20,7 +20,7 @@ module BitbucketServer
end
def browse_url
- # The JSON reponse contains an array of 1 element. Not sure if there
+ # The JSON response contains an array of 1 element. Not sure if there
# are cases where multiple links would be provided.
raw.dig('links', 'self').first.fetch('href')
end
diff --git a/lib/gitlab/background_migration/fix_cross_project_label_links.rb b/lib/gitlab/background_migration/fix_cross_project_label_links.rb
index 0a12401c35f..bf5d7f5f322 100644
--- a/lib/gitlab/background_migration/fix_cross_project_label_links.rb
+++ b/lib/gitlab/background_migration/fix_cross_project_label_links.rb
@@ -95,7 +95,7 @@ module Gitlab
local_labels = available_labels(project_id)
# get all label links for the given resource (issue/MR)
- # which reference a label not included in avaiable_labels
+ # which reference a label not included in available_labels
# (other than its project labels and labels of ancestor groups)
cross_labels = LabelLink
.select('label_id, labels.title as title, labels.color as color, label_links.id as label_link_id')
diff --git a/lib/gitlab/ci/pipeline/chain/limit/activity.rb b/lib/gitlab/ci/pipeline/chain/limit/activity.rb
index fe7c8738cc0..68482cf08a9 100644
--- a/lib/gitlab/ci/pipeline/chain/limit/activity.rb
+++ b/lib/gitlab/ci/pipeline/chain/limit/activity.rb
@@ -7,11 +7,11 @@ module Gitlab
module Limit
class Activity < Chain::Base
def perform!
- # to be overriden in EE
+ # to be overridden in EE
end
def break?
- false # to be overriden in EE
+ false # to be overridden in EE
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/limit/size.rb b/lib/gitlab/ci/pipeline/chain/limit/size.rb
index b4d51437cd6..cd330c58406 100644
--- a/lib/gitlab/ci/pipeline/chain/limit/size.rb
+++ b/lib/gitlab/ci/pipeline/chain/limit/size.rb
@@ -7,11 +7,11 @@ module Gitlab
module Limit
class Size < Chain::Base
def perform!
- # to be overriden in EE
+ # to be overridden in EE
end
def break?
- false # to be overriden in EE
+ false # to be overridden in EE
end
end
end
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 9d99d04d263..5f7af2ffc20 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -338,7 +338,6 @@ rollout 100%:
image_tag=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG}
fi
- replicas="1"
service_enabled="true"
postgres_enabled="$POSTGRES_ENABLED"
@@ -440,7 +439,9 @@ rollout 100%:
chart/
fi
- kubectl rollout status -n "$KUBE_NAMESPACE" -w "$ROLLOUT_RESOURCE_TYPE/$name"
+ if [[ -z "$ROLLOUT_STATUS_DISABLED" ]]; then
+ kubectl rollout status -n "$KUBE_NAMESPACE" -w "$ROLLOUT_RESOURCE_TYPE/$name"
+ fi
}
function scale() {
diff --git a/lib/gitlab/ci/templates/dotNET-Core.yml b/lib/gitlab/ci/templates/dotNET-Core.yml
index 558ca3d22e1..708b75f83e8 100644
--- a/lib/gitlab/ci/templates/dotNET-Core.yml
+++ b/lib/gitlab/ci/templates/dotNET-Core.yml
@@ -3,10 +3,11 @@
# ### Specify the Docker image
#
-# Instead of installing .NET Core SDK manually, a docker image is used
-# with already pre-installed .NET Core SDK.
-# The 'latest' tag targets the latest available version of .NET Core SDK image.
-# If preferred, you can explicitly specify version of .NET Core e.g. using '2.2-sdk' tag.
+# Instead of installing .NET Core SDK manually, a docker image is used
+# with already pre-installed .NET Core SDK.
+#
+# The 'latest' tag targets the latest available version of .NET Core SDK image.
+# If preferred, you can explicitly specify version of .NET Core (e.g. using '2.2-sdk' tag).
#
# See other available tags for .NET Core: https://hub.docker.com/r/microsoft/dotnet
# Learn more about Docker tags: https://docs.docker.com/glossary/?term=tag
@@ -17,16 +18,16 @@ image: microsoft/dotnet:latest
#
variables:
# 1) Name of directory where restore and build objects are stored.
- OBJECTS_DIRECTORY: 'obj'
- # 2) Name of directory used for keeping restored dependencies.
+ OBJECTS_DIRECTORY: 'obj'
+ # 2) Name of directory used for keeping restored dependencies.
NUGET_PACKAGES_DIRECTORY: '.nuget'
# 3) A relative path to the source code from project repository root.
# NOTE: Please edit this path so it matches the structure of your project!
- SOURCE_CODE_PATH: '*/*/'
+ SOURCE_CODE_PATH: '*/*/'
# ### Define stage list
#
-# In this example there are only two stages.
+# In this example there are only two stages.
# Initially, the project will be built and then tested.
stages:
- build
@@ -34,47 +35,55 @@ stages:
# ### Define global cache rule
#
-# Before building the project, all dependencies (e.g. third-party NuGet packages)
-# must be restored. Jobs on GitLab.com's Shared Runners are executed on autoscaled machines.
-# Each machine is used only once (for security reasons) and after that it is removed.
-# What that means is that before every job a dependency restore must be performed
+# Before building the project, all dependencies (e.g. third-party NuGet packages)
+# must be restored. Jobs on GitLab.com's Shared Runners are executed on autoscaled machines.
+#
+# Each machine is used only once (for security reasons) and after that is removed.
+# This means that, before every job, a dependency restore must be performed
# because restored dependencies are removed along with machines. Fortunately,
# GitLab provides cache mechanism with the aim of keeping restored dependencies
-# for other jobs. This example shows how to configure cache to pass over restored
+# for other jobs.
+#
+# This example shows how to configure cache to pass over restored
# dependencies for re-use.
#
# With global cache rule, cached dependencies will be downloaded before every job
# and then unpacked to the paths as specified below.
cache:
# Per-stage and per-branch caching.
- key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
+ key: "$CI_JOB_STAGE-$CI_COMMIT_REF_SLUG"
paths:
# Specify three paths that should be cached:
#
# 1) Main JSON file holding information about package dependency tree, packages versions,
# frameworks etc. It also holds information where to the dependencies were restored.
- - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json'
+ - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/project.assets.json'
# 2) Other NuGet and MSBuild related files. Also needed.
- - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*'
+ - '$SOURCE_CODE_PATH$OBJECTS_DIRECTORY/*.csproj.nuget.*'
# 3) Path to the directory where restored dependencies are kept.
- - '$NUGET_PACKAGES_DIRECTORY'
- # 'pull-push' policy means that latest cache will be downloaded (if exists)
- # before executing the job, and a newer version will be uploaded afterwards.
- # Such setting saves time when there are no changes in referenced third-party
- # packages. For example if you run a pipeline with changes in your code,
- # but with no changes within third-party packages which your project is using,
- # then project restore will happen in next to no time as all required dependencies
- # will already be there — unzipped from cache. 'pull-push' policy is a default
- # cache policy, you do not have to specify it explicitly.
- policy: pull-push
+ - '$NUGET_PACKAGES_DIRECTORY'
+ #
+ # 'pull-push' policy means that latest cache will be downloaded (if it exists)
+ # before executing the job, and a newer version will be uploaded afterwards.
+ # Such a setting saves time when there are no changes in referenced third-party
+ # packages.
+ #
+ # For example, if you run a pipeline with changes in your code,
+ # but with no changes within third-party packages which your project is using,
+ # then project restore will happen quickly as all required dependencies
+ # will already be there — unzipped from cache.
+
+ # 'pull-push' policy is the default cache policy, you do not have to specify it explicitly.
+ policy: pull-push
# ### Restore project dependencies
#
-# NuGet packages by default are restored to '.nuget/packages' directory
-# in the user's home directory. That directory is out of scope of GitLab caching.
-# To get around this a custom path can be specified using '--packages <PATH>' option
-# for 'dotnet restore' command. In this example a temporary directory is created
-# in the root of project repository, so it's content can be cached.
+# NuGet packages by default are restored to '.nuget/packages' directory
+# in the user's home directory. That directory is out of scope of GitLab caching.
+#
+# To get around this, a custom path can be specified using the '--packages <PATH>' option
+# for 'dotnet restore' command. In this example, a temporary directory is created
+# in the root of project repository, so its content can be cached.
#
# Learn more about GitLab cache: https://docs.gitlab.com/ee/ci/caching/index.html
before_script:
@@ -82,26 +91,26 @@ before_script:
build:
stage: build
- # ### Build all projects discovered from solution file.
+ # ### Build all projects discovered from solution file.
#
- # Note: this will fail if you have any projects in your solution that are not
- # .NET Core based projects e.g. WCF service, which is based on .NET Framework,
- # not .NET Core. In such scenario you will need to build every .NET Core based
- # project by explicitly specifying a relative path to the directory
- # where it is located e.g. 'dotnet build ./src/ConsoleApp'.
+ # Note: this will fail if you have any projects in your solution that are not
+ # .NET Core-based projects (e.g. WCF service), which is based on .NET Framework,
+ # not .NET Core. In this scenario, you will need to build every .NET Core-based
+ # project by explicitly specifying a relative path to the directory
+ # where it is located (e.g. 'dotnet build ./src/ConsoleApp').
# Only one project path can be passed as a parameter to 'dotnet build' command.
script:
- - 'dotnet build --no-restore'
+ - 'dotnet build --no-restore'
tests:
stage: test
# ### Run the tests
#
- # You can either run tests for all test projects that are defined in your solution
+ # You can either run tests for all test projects that are defined in your solution
# with 'dotnet test' or run tests only for specific project by specifying
- # a relative path to the directory where it is located e.g. 'dotnet test ./test/UnitTests'.
+ # a relative path to the directory where it is located (e.g. 'dotnet test ./test/UnitTests').
#
# You may want to define separate testing jobs for different types of testing
- # e.g. integration tests, unit tests etc.
+ # (e.g. integration tests, unit tests etc).
script:
- 'dotnet test --no-restore'
diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb
index df34d254c65..6796fcce75f 100644
--- a/lib/gitlab/config/entry/validators.rb
+++ b/lib/gitlab/config/entry/validators.rb
@@ -36,10 +36,10 @@ module Gitlab
class AllowedArrayValuesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- unkown_values = value - options[:in]
- unless unkown_values.empty?
+ unknown_values = value - options[:in]
+ unless unknown_values.empty?
record.errors.add(attribute, "contains unknown values: " +
- unkown_values.join(', '))
+ unknown_values.join(', '))
end
end
end
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index 3ef19d801b7..f0ca397609d 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -121,6 +121,8 @@ module Gitlab
\.prettierrc |
\.scss-lint.yml |
\.stylelintrc |
+ \.haml-lint.yml |
+ \.haml-lint_todo.yml |
babel\.config\.js |
jest\.config\.js |
karma\.config\.js |
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index fc9bcbdcca2..455588f3c66 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -500,7 +500,7 @@ module Gitlab
end
# Return total diverging commits count
- def diverging_commit_count(from, to, max_count:)
+ def diverging_commit_count(from, to, max_count: 0)
wrapped_gitaly_errors do
gitaly_commit_client.diverging_commit_count(from, to, max_count: max_count)
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 2896b7e1ce0..d21b98d36ea 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -79,7 +79,7 @@ module Gitlab
def tree_entry(ref, path, limit = nil)
if Pathname.new(path).cleanpath.to_s.start_with?('../')
- # The TreeEntry RPC should return an empty reponse in this case but in
+ # The TreeEntry RPC should return an empty response in this case but in
# Gitaly 0.107.0 and earlier we get an exception instead. This early return
# saves us a Gitaly roundtrip while also avoiding the exception.
return
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index c6d4fda4af5..7bbcb53f016 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -176,6 +176,8 @@ excluded_attributes:
- :enabled
methods:
+ notes:
+ - :type
labels:
- :type
label:
diff --git a/lib/gitlab/lets_encrypt/challenge.rb b/lib/gitlab/lets_encrypt/challenge.rb
new file mode 100644
index 00000000000..6a7f5e965c5
--- /dev/null
+++ b/lib/gitlab/lets_encrypt/challenge.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module LetsEncrypt
+ class Challenge
+ def initialize(acme_challenge)
+ @acme_challenge = acme_challenge
+ end
+
+ delegate :url, :token, :file_content, :status, :request_validation, to: :acme_challenge
+
+ private
+
+ attr_reader :acme_challenge
+ end
+ end
+end
diff --git a/lib/gitlab/lets_encrypt/client.rb b/lib/gitlab/lets_encrypt/client.rb
new file mode 100644
index 00000000000..d7468b06767
--- /dev/null
+++ b/lib/gitlab/lets_encrypt/client.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module LetsEncrypt
+ class Client
+ PRODUCTION_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory'
+ STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory'
+
+ def new_order(domain_name)
+ ensure_account
+
+ acme_order = acme_client.new_order(identifiers: [domain_name])
+
+ ::Gitlab::LetsEncrypt::Order.new(acme_order)
+ end
+
+ def load_order(url)
+ ensure_account
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ ::Gitlab::LetsEncrypt::Order.new(acme_client.order(url: url))
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ def load_challenge(url)
+ ensure_account
+
+ ::Gitlab::LetsEncrypt::Challenge.new(acme_client.challenge(url: url))
+ end
+
+ def terms_of_service_url
+ acme_client.terms_of_service
+ end
+
+ def enabled?
+ return false unless Feature.enabled?(:pages_auto_ssl)
+
+ Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted
+ end
+
+ private
+
+ def acme_client
+ @acme_client ||= ::Acme::Client.new(private_key: private_key, directory: acme_api_directory_url)
+ end
+
+ def private_key
+ @private_key ||= OpenSSL::PKey.read(Gitlab::Application.secrets.lets_encrypt_private_key)
+ end
+
+ def admin_email
+ Gitlab::CurrentSettings.lets_encrypt_notification_email
+ end
+
+ def contact
+ "mailto:#{admin_email}"
+ end
+
+ def ensure_account
+ raise 'Acme integration is disabled' unless enabled?
+
+ @acme_account ||= acme_client.new_account(contact: contact, terms_of_service_agreed: true)
+ end
+
+ def acme_api_directory_url
+ if Rails.env.production?
+ PRODUCTION_DIRECTORY_URL
+ else
+ STAGING_DIRECTORY_URL
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/lets_encrypt/order.rb b/lib/gitlab/lets_encrypt/order.rb
new file mode 100644
index 00000000000..5109b5e9843
--- /dev/null
+++ b/lib/gitlab/lets_encrypt/order.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module LetsEncrypt
+ class Order
+ def initialize(acme_order)
+ @acme_order = acme_order
+ end
+
+ def new_challenge
+ authorization = @acme_order.authorizations.first
+ challenge = authorization.http
+ ::Gitlab::LetsEncrypt::Challenge.new(challenge)
+ end
+
+ delegate :url, :status, to: :acme_order
+
+ private
+
+ attr_reader :acme_order
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index 4d9c43f37e7..18a69321905 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -23,32 +23,25 @@ module Gitlab
end
def init_metrics
- metrics = {
- file_descriptors: ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum),
- memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum),
- process_cpu_seconds_total: ::Gitlab::Metrics.gauge(with_prefix(:process, :cpu_seconds_total), 'Process CPU seconds total'),
- process_max_fds: ::Gitlab::Metrics.gauge(with_prefix(:process, :max_fds), 'Process max fds'),
- process_resident_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :resident_memory_bytes), 'Memory used', labels, :livesum),
- process_start_time_seconds: ::Gitlab::Metrics.gauge(with_prefix(:process, :start_time_seconds), 'Process start time seconds'),
- sampler_duration: ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels),
- total_time: ::Gitlab::Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels)
- }
-
+ metrics = {}
+ metrics[:sampler_duration] = ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels)
+ metrics[:total_time] = ::Gitlab::Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels)
GC.stat.keys.each do |key|
metrics[key] = ::Gitlab::Metrics.gauge(with_prefix(:gc_stat, key), to_doc_string(key), labels, :livesum)
end
+ metrics[:memory_usage] = ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum)
+ metrics[:file_descriptors] = ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum)
+
metrics
end
def sample
start_time = System.monotonic_time
+ metrics[:memory_usage].set(labels.merge(worker_label), System.memory_usage)
metrics[:file_descriptors].set(labels.merge(worker_label), System.file_descriptor_count)
- metrics[:process_cpu_seconds_total].set(labels.merge(worker_label), ::Gitlab::Metrics::System.cpu_time)
- metrics[:process_max_fds].set(labels.merge(worker_label), ::Gitlab::Metrics::System.max_open_file_descriptors)
- metrics[:process_start_time_seconds].set(labels.merge(worker_label), ::Gitlab::Metrics::System.process_start_time)
- set_memory_usage_metrics
+
sample_gc
metrics[:sampler_duration].increment(labels, System.monotonic_time - start_time)
@@ -68,14 +61,6 @@ module Gitlab
metrics[:total_time].increment(labels, GC::Profiler.total_time)
end
- def set_memory_usage_metrics
- memory_usage = System.memory_usage
- memory_labels = labels.merge(worker_label)
-
- metrics[:memory_bytes].set(memory_labels, memory_usage)
- metrics[:process_resident_memory_bytes].set(memory_labels, memory_usage)
- end
-
def worker_label
return {} unless defined?(Unicorn::Worker)
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
index 1b6c52ac0bf..bec64e864b3 100644
--- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb
+++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
@@ -8,16 +8,12 @@ module Gitlab
super(interval)
end
- def metrics
- @metrics ||= init_metrics
+ def unicorn_active_connections
+ @unicorn_active_connections ||= ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max)
end
- def init_metrics
- {
- unicorn_active_connections: ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max),
- unicorn_queued_connections: ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max),
- unicorn_workers: ::Gitlab::Metrics.gauge(:unicorn_workers, 'Unicorn workers')
- }
+ def unicorn_queued_connections
+ @unicorn_queued_connections ||= ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max)
end
def enabled?
@@ -27,13 +23,14 @@ module Gitlab
def sample
Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
- set_unicorn_connection_metrics('tcp', addr, stats)
+ unicorn_active_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.active)
+ unicorn_queued_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.queued)
end
+
Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
- set_unicorn_connection_metrics('unix', addr, stats)
+ unicorn_active_connections.set({ socket_type: 'unix', socket_address: addr }, stats.active)
+ unicorn_queued_connections.set({ socket_type: 'unix', socket_address: addr }, stats.queued)
end
-
- metrics[:unicorn_workers].set({}, unicorn_workers_count)
end
private
@@ -42,13 +39,6 @@ module Gitlab
@tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
end
- def set_unicorn_connection_metrics(type, addr, stats)
- labels = { socket_type: type, socket_address: addr }
-
- metrics[:unicorn_active_connections].set(labels, stats.active)
- metrics[:unicorn_queued_connections].set(labels, stats.queued)
- end
-
def unix_listeners
@unix_listeners ||= Unicorn.listener_names - tcp_listeners
end
@@ -56,10 +46,6 @@ module Gitlab
def unicorn_with_listeners?
defined?(Unicorn) && Unicorn.listener_names.any?
end
-
- def unicorn_workers_count
- Sys::ProcTable.ps.select {|p| p.cmdline.match(/unicorn_rails worker.+ #{Rails.root.to_s}/)}.count
- end
end
end
end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index ce0bb82d138..426496855e3 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -23,21 +23,6 @@ module Gitlab
def self.file_descriptor_count
Dir.glob('/proc/self/fd/*').length
end
-
- def self.max_open_file_descriptors
- match = File.read('/proc/self/limits').match(/Max open files\s*(\d+)/)
-
- return unless match && match[1]
-
- match[1].to_i
- end
-
- def self.process_start_time
- start_time_in_jiffies = Sys::ProcTable.ps(pid: Process.pid).starttime
- return 0 unless start_time_in_jiffies
-
- start_time_in_jiffies / 100
- end
else
def self.memory_usage
0.0
@@ -46,14 +31,6 @@ module Gitlab
def self.file_descriptor_count
0
end
-
- def self.max_open_file_descriptors
- 0
- end
-
- def self.process_start_time
- 0
- end
end
# THREAD_CPUTIME is not supported on OS X
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index a07b1246bee..aa2c1ac9cef 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -58,6 +58,7 @@ module Gitlab
uploads
users
v2
+ visual-review-toolbar.js
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
diff --git a/lib/gitlab/routing.rb b/lib/gitlab/routing.rb
index 3b05f181ed2..84885be9bda 100644
--- a/lib/gitlab/routing.rb
+++ b/lib/gitlab/routing.rb
@@ -45,7 +45,7 @@ module Gitlab
def self.redirect_legacy_paths(router, *paths)
build_redirect_path = lambda do |request, _params, path|
- # Only replace the last occurence of `path`.
+ # Only replace the last occurrence of `path`.
#
# `request.fullpath` includes the querystring
new_path = request.path.sub(%r{/#{path}(/*)(?!.*#{path})}, "/-/#{path}\\1")
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 356e6445e0e..72c44114001 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -10,7 +10,7 @@ module Gitlab
def self.context(current_user = nil)
return unless enabled?
- Raven.tags_context(locale: I18n.locale)
+ Raven.tags_context(default_tags)
if current_user
Raven.user_context(
@@ -44,16 +44,19 @@ module Gitlab
extra[:issue_url] = issue_url if issue_url
context # Make sure we've set everything we know in the context
- tags = {
- Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id
- }
-
- Raven.capture_exception(exception, tags: tags, extra: extra)
+ Raven.capture_exception(exception, tags: default_tags, extra: extra)
end
end
def self.should_raise_for_dev?
Rails.env.development? || Rails.env.test?
end
+
+ def self.default_tags
+ {
+ Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id,
+ locale: I18n.locale
+ }
+ end
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 08156d7ffa6..9aa2e972adf 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -26,7 +26,7 @@ module Gitlab
uuid: Gitlab::CurrentSettings.uuid,
hostname: Gitlab.config.gitlab.host,
version: Gitlab::VERSION,
- installation_type: Gitlab::INSTALLATION_TYPE,
+ installation_type: installation_type,
active_user_count: count(User.active),
recorded_at: Time.now,
edition: 'CE'
@@ -81,6 +81,7 @@ module Gitlab
milestone_lists: count(List.milestone),
milestones: count(Milestone),
pages_domains: count(PagesDomain),
+ pool_repositories: count(PoolRepository),
projects: count(Project),
projects_imported_from_github: count(Project.where(import_type: 'github')),
projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)),
@@ -190,6 +191,14 @@ module Gitlab
result[key] = approx_counts[model] || -1
end
end
+
+ def installation_type
+ if Rails.env.production?
+ Gitlab::INSTALLATION_TYPE
+ else
+ "gitlab-development-kit"
+ end
+ end
end
end
end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index e2083848a8d..722e3e04d1c 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -122,7 +122,7 @@ module Mattermost
@oauth_uri = nil
- response = get('/oauth/gitlab/login', follow_redirects: false, format: 'text/html')
+ response = get('/oauth/gitlab/login', follow_redirects: false)
return unless (300...400) === response.code
redirect_uri = response.headers['location']
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index 2f2de083dc0..32df74f104a 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -1,8 +1,8 @@
#! /bin/sh
# GITLAB
-# Maintainer: @randx
-# Authors: rovanion.luckey@gmail.com, @randx
+# Maintainer: @dzaporozhets
+# Authors: rovanion.luckey@gmail.com, @dzaporozhets
### BEGIN INIT INFO
# Provides: gitlab
@@ -26,6 +26,7 @@
### Environment variables
RAILS_ENV="production"
+EXPERIMENTAL_PUMA=""
# Script variable names should be lower-case not to conflict with
# internal /bin/sh variables such as PATH, EDITOR or SHELL.
@@ -75,7 +76,7 @@ check_pids(){
echo "Could not create the path $pid_path needed to store the pids."
exit 1
fi
- # If there exists a file which should hold the value of the Unicorn pid: read it.
+ # If there exists a file which should hold the value of the web server pid: read it.
if [ -f "$web_server_pid_path" ]; then
wpid=$(cat "$web_server_pid_path")
else
@@ -198,7 +199,7 @@ check_stale_pids(){
# If there is a pid it is something else than 0, the service is running if
# *_status is == 0.
if [ "$wpid" != "0" ] && [ "$web_status" != "0" ]; then
- echo "Removing stale Unicorn web server pid. This is most likely caused by the web server crashing the last time it ran."
+ echo "Removing stale web server pid. This is most likely caused by the web server crashing the last time it ran."
if ! rm "$web_server_pid_path"; then
echo "Unable to remove stale pid, exiting."
exit 1
@@ -250,12 +251,12 @@ exit_if_not_running(){
fi
}
-## Starts Unicorn and Sidekiq if they're not running.
+## Starts web server and Sidekiq if they're not running.
start_gitlab() {
check_stale_pids
if [ "$web_status" != "0" ]; then
- echo "Starting GitLab Unicorn"
+ echo "Starting GitLab web server"
fi
if [ "$sidekiq_status" != "0" ]; then
echo "Starting GitLab Sidekiq"
@@ -275,12 +276,12 @@ start_gitlab() {
# Then check if the service is running. If it is: don't start again.
if [ "$web_status" = "0" ]; then
- echo "The Unicorn web server already running with pid $wpid, not restarting."
+ echo "The web server already running with pid $wpid, not restarting."
else
# Remove old socket if it exists
rm -f "$rails_socket" 2>/dev/null
# Start the web server
- RAILS_ENV=$RAILS_ENV bin/web start
+ RAILS_ENV=$RAILS_ENV EXPERIMENTAL_PUMA=$EXPERIMENTAL_PUMA bin/web start
fi
# If sidekiq is already running, don't start it again.
@@ -336,13 +337,13 @@ start_gitlab() {
print_status
}
-## Asks Unicorn, Sidekiq and MailRoom if they would be so kind as to stop, if not kills them.
+## Asks web server, Sidekiq and MailRoom if they would be so kind as to stop, if not kills them.
stop_gitlab() {
exit_if_not_running
if [ "$web_status" = "0" ]; then
- echo "Shutting down GitLab Unicorn"
- RAILS_ENV=$RAILS_ENV bin/web stop
+ echo "Shutting down GitLab web server"
+ RAILS_ENV=$RAILS_ENV EXPERIMENTAL_PUMA=$EXPERIMENTAL_PUMA bin/web stop
fi
if [ "$sidekiq_status" = "0" ]; then
echo "Shutting down GitLab Sidekiq"
@@ -398,9 +399,9 @@ print_status() {
return
fi
if [ "$web_status" = "0" ]; then
- echo "The GitLab Unicorn web server with pid $wpid is running."
+ echo "The GitLab web server with pid $wpid is running."
else
- printf "The GitLab Unicorn web server is \033[31mnot running\033[0m.\n"
+ printf "The GitLab web server is \033[31mnot running\033[0m.\n"
fi
if [ "$sidekiq_status" = "0" ]; then
echo "The GitLab Sidekiq job dispatcher with pid $spid is running."
@@ -438,15 +439,15 @@ print_status() {
fi
}
-## Tells unicorn to reload its config and Sidekiq to restart
+## Tells web server to reload its config and Sidekiq to restart
reload_gitlab(){
exit_if_not_running
if [ "$wpid" = "0" ];then
- echo "The GitLab Unicorn Web server is not running thus its configuration can't be reloaded."
+ echo "The GitLab web server Web server is not running thus its configuration can't be reloaded."
exit 1
fi
- printf "Reloading GitLab Unicorn configuration... "
- RAILS_ENV=$RAILS_ENV bin/web reload
+ printf "Reloading GitLab web server configuration... "
+ RAILS_ENV=$RAILS_ENV EXPERIMENTAL_PUMA=$EXPERIMENTAL_PUMA bin/web reload
echo "Done."
echo "Restarting GitLab Sidekiq since it isn't capable of reloading its config..."
@@ -461,7 +462,7 @@ reload_gitlab(){
print_status
}
-## Restarts Sidekiq and Unicorn.
+## Restarts Sidekiq and web server.
restart_gitlab(){
check_status
if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; } || { [ "$gitaly_enabled" = true ] && [ "$gitaly_status" = "0" ]; }; then
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index 295c79fccfc..ab41dba3017 100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -5,6 +5,9 @@
# Normal values are "production", "test" and "development".
RAILS_ENV="production"
+# Uncomment the line below to enable Puma web server instead of Unicorn.
+# EXPERIMENTAL_PUMA=1
+
# app_user defines the user that GitLab is run as.
# The default is "git".
app_user="git"
diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb
index 46aad8aa885..c36cacbaf4f 100644
--- a/lib/system_check/base_check.rb
+++ b/lib/system_check/base_check.rb
@@ -121,7 +121,7 @@ module SystemCheck
#
# @see #try_fixing_it
# @see #fix_and_rerun
- # @see #for_more_infromation
+ # @see #for_more_information
def show_error
raise NotImplementedError
end
diff --git a/lib/tasks/gitlab/features.rake b/lib/tasks/gitlab/features.rake
index d115961108e..d88bcca0819 100644
--- a/lib/tasks/gitlab/features.rake
+++ b/lib/tasks/gitlab/features.rake
@@ -17,7 +17,7 @@ namespace :gitlab do
if status
Feature.enable(flag)
else
- Feature.disable(flag)
+ Feature.get(flag).remove
end
end
end
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
index c5d0f2c292f..2353b2dc659 100644
--- a/lib/tasks/lint.rake
+++ b/lib/tasks/lint.rake
@@ -37,32 +37,15 @@ unless Rails.env.production?
lint:static_verification
].each do |task|
pid = Process.fork do
- rd_out, wr_out = IO.pipe
- rd_err, wr_err = IO.pipe
- stdout = $stdout.dup
- stderr = $stderr.dup
- $stdout.reopen(wr_out)
- $stderr.reopen(wr_err)
-
- begin
- Rake::Task[task].invoke
- rescue SystemExit => ex
- msg = "*** Rake task #{task} exited:"
- raise ex
- rescue => ex
- msg = "*** Rake task #{task} raised #{ex.class}:"
- raise ex
- ensure
- $stdout.reopen(stdout)
- $stderr.reopen(stderr)
- wr_out.close
- wr_err.close
-
- warn "\n#{msg}\n\n" if msg
-
- IO.copy_stream(rd_out, $stdout)
- IO.copy_stream(rd_err, $stderr)
- end
+ puts "*** Running rake task: #{task} ***"
+
+ Rake::Task[task].invoke
+ rescue SystemExit => ex
+ warn "!!! Rake task #{task} exited:"
+ raise ex
+ rescue StandardError, ScriptError => ex
+ warn "!!! Rake task #{task} raised #{ex.class}:"
+ raise ex
end
Process.waitpid(pid)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 71ec8bcb9ba..c20cae9665a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -868,6 +868,9 @@ msgstr ""
msgid "An error occurred while fetching the board lists. Please try again."
msgstr ""
+msgid "An error occurred while fetching the builds."
+msgstr ""
+
msgid "An error occurred while fetching the job log."
msgstr ""
@@ -883,6 +886,9 @@ msgstr ""
msgid "An error occurred while fetching the releases. Please try again."
msgstr ""
+msgid "An error occurred while fetching this tab."
+msgstr ""
+
msgid "An error occurred while getting projects"
msgstr ""
@@ -970,9 +976,15 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Milestone"
+msgstr ""
+
msgid "Any encrypted tokens"
msgstr ""
+msgid "Any namespace"
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -1060,6 +1072,12 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
+msgid "Are you sure you want to cancel creating this comment?"
+msgstr ""
+
+msgid "Are you sure you want to cancel editing this comment?"
+msgstr ""
+
msgid "Are you sure you want to delete this list?"
msgstr ""
@@ -2578,6 +2596,9 @@ msgstr ""
msgid "Comment form position"
msgstr ""
+msgid "Comment is being updated"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -3599,6 +3620,12 @@ msgstr ""
msgid "EmailError|Your account has been blocked. If you believe this is in error, contact a staff member."
msgstr ""
+msgid "EmailToken|reset it"
+msgstr ""
+
+msgid "EmailToken|resetting..."
+msgstr ""
+
msgid "Emails"
msgstr ""
@@ -3905,6 +3932,9 @@ msgstr ""
msgid "Error loading merge requests."
msgstr ""
+msgid "Error loading milestone tab"
+msgstr ""
+
msgid "Error loading project data. Please try again."
msgstr ""
@@ -4193,6 +4223,9 @@ msgstr ""
msgid "Failed to load errors from Sentry. Error message: %{errorMessage}"
msgstr ""
+msgid "Failed to load related branches"
+msgstr ""
+
msgid "Failed to promote label due to internal error. Please contact administrators."
msgstr ""
@@ -4872,7 +4905,7 @@ msgstr ""
msgid "I accept the|Terms of Service and Privacy Policy"
msgstr ""
-msgid "I have read and agree to the Let's Encrypt Terms of Service"
+msgid "I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end}"
msgstr ""
msgid "ID"
@@ -5217,6 +5250,9 @@ msgstr ""
msgid "Issue events"
msgstr ""
+msgid "Issue update failed"
+msgstr ""
+
msgid "IssueBoards|Board"
msgstr ""
@@ -5899,7 +5935,7 @@ msgstr ""
msgid "MergeRequest|Error loading full diff. Please try again."
msgstr ""
-msgid "MergeRequest|Filter files"
+msgid "MergeRequest|Filter files or search with %{modifier_key}+p"
msgstr ""
msgid "MergeRequest|No files found"
@@ -6022,6 +6058,9 @@ msgstr ""
msgid "Mirroring settings were successfully updated."
msgstr ""
+msgid "Missing commit signatures endpoint!"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
@@ -6387,6 +6426,9 @@ msgstr ""
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
+msgid "NoteForm|Note"
+msgstr ""
+
msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr ""
@@ -6402,6 +6444,9 @@ msgstr ""
msgid "Notes|Show history only"
msgstr ""
+msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost"
+msgstr ""
+
msgid "Nothing to preview."
msgstr ""
@@ -7238,9 +7283,6 @@ msgstr ""
msgid "Profiles|The maximum file size allowed is 200KB."
msgstr ""
-msgid "Profiles|There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
-msgstr ""
-
msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
msgstr ""
@@ -8026,6 +8068,9 @@ msgstr ""
msgid "Reports|Class"
msgstr ""
+msgid "Reports|Classname"
+msgstr ""
+
msgid "Reports|Execution time"
msgstr ""
@@ -8337,6 +8382,9 @@ msgstr ""
msgid "Search files"
msgstr ""
+msgid "Search for a group"
+msgstr ""
+
msgid "Search for projects, issues, etc."
msgstr ""
@@ -9104,6 +9152,9 @@ msgstr ""
msgid "Storage:"
msgstr ""
+msgid "StorageSize|Unknown"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
@@ -10310,6 +10361,9 @@ msgstr ""
msgid "Unable to schedule a pipeline to run immediately"
msgstr ""
+msgid "Unable to update this issue at this time."
+msgstr ""
+
msgid "Unarchive project"
msgstr ""
@@ -11149,6 +11203,9 @@ msgstr ""
msgid "You need permission."
msgstr ""
+msgid "You need to be logged in."
+msgstr ""
+
msgid "You need to register a two-factor authentication app before you can set up a U2F device."
msgstr ""
@@ -11275,6 +11332,12 @@ msgstr ""
msgid "Your changes have been successfully committed."
msgstr ""
+msgid "Your comment could not be submitted! Please check your network connection and try again."
+msgstr ""
+
+msgid "Your comment could not be updated! Please check your network connection and try again."
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
diff --git a/package.json b/package.json
index eb557101662..f46ac88141c 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,17 @@
{
"private": true,
"scripts": {
+ "check-dependencies": "yarn check --integrity",
"clean": "rm -rf public/assets tmp/cache/*-loader",
"dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
"eslint": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
+ "prejest": "yarn check-dependencies",
+ "jest": "jest",
"jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"jsdoc": "jsdoc -c config/jsdocs.config.js",
+ "prekarma": "yarn check-dependencies",
"karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
"karma-start": "BABEL_ENV=karma karma start config/karma.config.js",
@@ -178,7 +182,7 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^4.0.0-beta.0",
"md5": "^2.2.1",
- "node-sass": "^4.11.0",
+ "node-sass": "^4.12.0",
"nodemon": "^1.18.9",
"pixelmatch": "^4.0.2",
"postcss": "^7.0.14",
diff --git a/public/visual-review-toolbar.js b/public/visual-review-toolbar.js
new file mode 100644
index 00000000000..dc21197bda9
--- /dev/null
+++ b/public/visual-review-toolbar.js
@@ -0,0 +1,626 @@
+///////////////////////////////////////////////
+/////////////////// STYLES ////////////////////
+///////////////////////////////////////////////
+
+const buttonClearStyles = `
+ -webkit-appearance: none;
+`;
+
+const buttonBaseStyles = `
+ cursor: pointer;
+ transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear;
+`;
+
+const buttonSuccessActiveStyles = `
+ background-color: #168f48;
+ border-color: #12753a;
+ color: #fff;
+`;
+
+const buttonSuccessHoverStyles = `
+ color: #fff;
+ background-color: #137e3f;
+ border-color: #127339;
+`;
+
+const buttonSuccessStyles = `
+ ${buttonBaseStyles}
+ background-color: #1aaa55;
+ border: 1px solid #168f48;
+ color: #fff;
+`;
+
+const buttonSecondaryStyles = `
+ ${buttonBaseStyles}
+ background: none #fff;
+ margin: 0 .5rem;
+ border: 1px solid #e3e3e3;
+`;
+
+const buttonSecondaryActiveStyles = `
+ color: #2e2e2e;
+ background-color: #e1e1e1;
+ border-color: #dadada;
+`;
+
+const buttonSecondaryHoverStyles = `
+ background-color: #f0f0f0;
+ border-color: #e3e3e3;
+ color: #2e2e2e;
+`;
+
+const buttonWideStyles = `
+ width: 100%;
+`;
+
+const buttonWrapperStyles = `
+ margin-top: 1rem;
+ display: flex;
+ align-items: baseline;
+ justify-content: flex-end;
+`;
+
+const collapseStyles = `
+ ${buttonBaseStyles}
+ width: 2.4rem;
+ height: 2.2rem;
+ margin-left: 1rem;
+ padding: .5rem;
+`;
+
+const collapseClosedStyles = `
+ ${collapseStyles}
+ align-self: center;
+`;
+
+const collapseOpenStyles = `
+ ${collapseStyles}
+`;
+
+const checkboxLabelStyles = `
+ padding: 0 .2rem;
+`;
+
+const checkboxWrapperStyles = `
+ display: flex;
+ align-items: baseline;
+`;
+
+const formStyles = `
+ display: flex;
+ flex-direction: column;
+ width: 100%
+`;
+
+const labelStyles = `
+ font-weight: 600;
+ display: inline-block;
+ width: 100%;
+`;
+
+const linkStyles = `
+ color: #1b69b6;
+ text-decoration: none;
+ background-color: transparent;
+ background-image: none;
+`;
+
+const messageStyles = `
+ padding: .25rem 0;
+ margin: 0;
+ line-height: 1.2rem;
+`;
+
+const metadataNoteStyles = `
+ font-size: .7rem;
+ line-height: 1rem;
+ color: #666;
+ margin-bottom: 0;
+`;
+
+const inputStyles = `
+ width: 100%;
+ border: 1px solid #dfdfdf;
+ border-radius: 4px;
+ padding: .1rem .2rem;
+ min-height: 2rem;
+ max-width: 17rem;
+`;
+
+const svgInnerStyles = `
+ pointer-events: none;
+`;
+
+const wrapperClosedStyles = `
+ max-width: 3.4rem;
+ max-height: 3.4rem;
+`;
+
+const wrapperOpenStyles = `
+ max-width: 22rem;
+ max-height: 22rem;
+`;
+
+const wrapperStyles = `
+ max-width: 22rem;
+ max-height: 22rem;
+ overflow: scroll;
+ position: fixed;
+ bottom: 1rem;
+ right: 1rem;
+ display: flex;
+ flex-direction: row-reverse;
+ padding: 1rem;
+ background-color: #fff;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
+ 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+ font-size: .8rem;
+ font-weight: 400;
+ color: #2e2e2e;
+`;
+
+const gitlabStyles = `
+ #gitlab-collapse > * {
+ ${svgInnerStyles}
+ }
+
+ #gitlab-form-wrapper {
+ ${formStyles}
+ }
+
+ #gitlab-review-container {
+ ${wrapperStyles}
+ }
+
+ .gitlab-open-wrapper {
+ ${wrapperOpenStyles}
+ }
+
+ .gitlab-closed-wrapper {
+ ${wrapperClosedStyles}
+ }
+
+ .gitlab-button-secondary {
+ ${buttonSecondaryStyles}
+ }
+
+ .gitlab-button-secondary:hover {
+ ${buttonSecondaryHoverStyles}
+ }
+
+ .gitlab-button-secondary:active {
+ ${buttonSecondaryActiveStyles}
+ }
+
+ .gitlab-button-success:hover {
+ ${buttonSuccessHoverStyles}
+ }
+
+ .gitlab-button-success:active {
+ ${buttonSuccessActiveStyles}
+ }
+
+ .gitlab-button-success {
+ ${buttonSuccessStyles}
+ }
+
+ .gitlab-button-wide {
+ ${buttonWideStyles}
+ }
+
+ .gitlab-button-wrapper {
+ ${buttonWrapperStyles}
+ }
+
+ .gitlab-collapse-closed {
+ ${collapseClosedStyles}
+ }
+
+ .gitlab-collapse-open {
+ ${collapseOpenStyles}
+ }
+
+ .gitlab-checkbox-label {
+ ${checkboxLabelStyles}
+ }
+
+ .gitlab-checkbox-wrapper {
+ ${checkboxWrapperStyles}
+ }
+
+ .gitlab-label {
+ ${labelStyles}
+ }
+
+ .gitlab-link {
+ ${linkStyles}
+ }
+
+ .gitlab-message {
+ ${messageStyles}
+ }
+
+ .gitlab-metadata-note {
+ ${metadataNoteStyles}
+ }
+
+ .gitlab-input {
+ ${inputStyles}
+ }
+`;
+
+function addStylesheet() {
+ const styleEl = document.createElement('style');
+ styleEl.insertAdjacentHTML('beforeend', gitlabStyles);
+ document.head.appendChild(styleEl);
+}
+
+///////////////////////////////////////////////
+/////////////////// STATE ////////////////////
+///////////////////////////////////////////////
+const data = {};
+
+///////////////////////////////////////////////
+///////////////// COMPONENTS //////////////////
+///////////////////////////////////////////////
+const note = `
+ <p id='gitlab-validation-note' class='gitlab-message'></p>
+`;
+
+const comment = `
+ <div>
+ <textarea id='gitlab-comment' name='gitlab-comment' rows='3' placeholder='Enter your feedback or idea' class='gitlab-input'></textarea>
+ ${note}
+ <p class='gitlab-metadata-note'>Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p>
+ </div>
+ <div class='gitlab-button-wrapper''>
+ <button class='gitlab-button-secondary' style='${buttonClearStyles}' type='button' id='gitlab-logout-button'> Logout </button>
+ <button class='gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-comment-button'> Send feedback </button>
+ </div>
+`;
+
+const commentIcon = `
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg>
+`
+
+const compressIcon = `
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg>
+`;
+
+const collapseButton = `
+ <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-collapse-open gitlab-button-secondary'>${compressIcon}</button>
+`;
+
+
+const form = (content) => `
+ <div id='gitlab-form-wrapper'>
+ ${content}
+ </div>
+`;
+
+const login = `
+ <div>
+ <label for='gitlab-token' class='gitlab-label'>Enter your <a class='gitlab-link' href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a></label>
+ <input class='gitlab-input' type='password' id='gitlab-token' name='gitlab-token'>
+ ${note}
+ </div>
+ <div class='gitlab-checkbox-wrapper'>
+ <input type="checkbox" id="remember_token" name="remember_token" value="remember">
+ <label for="remember_token" class='gitlab-checkbox-label'>Remember me</label>
+ </div>
+ <div class='gitlab-button-wrapper'>
+ <button class='gitlab-button-wide gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-login'> Submit </button>
+ </div>
+`;
+
+///////////////////////////////////////////////
+//////////////// INTERACTIONS /////////////////
+///////////////////////////////////////////////
+
+// from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator
+function getBrowserId (sUsrAg) {
+ var aKeys = ["MSIE", "Edge", "Firefox", "Safari", "Chrome", "Opera"],
+ nIdx = aKeys.length - 1;
+
+ for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx--);
+ return aKeys[nIdx];
+}
+
+function addCommentForm () {
+ const formWrapper = document.getElementById('gitlab-form-wrapper');
+ formWrapper.innerHTML = comment;
+}
+
+function addLoginForm () {
+ const formWrapper = document.getElementById('gitlab-form-wrapper');
+ formWrapper.innerHTML = login;
+}
+
+function authorizeUser () {
+ // Clear any old errors
+ clearNote('gitlab-token');
+
+ const token = document.getElementById('gitlab-token').value;
+ const rememberMe = document.getElementById('remember_token').checked;
+
+ if (!token) {
+ postError('Please enter your token.', 'gitlab-token');
+ return;
+ }
+
+ if (rememberMe) {
+ storeToken(token);
+ }
+
+ authSuccess(token);
+ return;
+}
+
+function authSuccess (token) {
+ data.token = token;
+ addCommentForm();
+}
+
+
+function clearNote (inputId) {
+ const note = document.getElementById('gitlab-validation-note');
+ note.innerText = '';
+ note.style.color = '';
+
+ if (inputId) {
+ const field = document.getElementById(inputId);
+ field.style.borderColor = '';
+ }
+}
+
+function confirmAndClear (discussionId) {
+ const commentButton = document.getElementById('gitlab-comment-button');
+ const note = document.getElementById('gitlab-validation-note');
+
+ commentButton.innerText = 'Feedback sent';
+ note.innerText = `Your comment was successfully posted to issue #${discussionId}`;
+
+ setTimeout(resetCommentButton, 1000);
+}
+
+function getInitialState () {
+ const { localStorage } = window;
+
+ try {
+ let token = localStorage.getItem('token');
+
+ if (token) {
+ data.token = token;
+ return comment;
+ }
+
+ return login;
+
+ } catch (err) {
+ return login;
+ }
+}
+
+function getProjectDetails () {
+ const { innerWidth,
+ innerHeight,
+ location: { href },
+ navigator: {
+ platform, userAgent
+ } } = window;
+ const browser = getBrowserId(userAgent);
+
+ const scriptEl = document.getElementById('review-app-toolbar-script')
+ const { projectId, discussionId, mrUrl } = scriptEl.dataset;
+
+ return {
+ href,
+ platform,
+ browser,
+ userAgent,
+ innerWidth,
+ innerHeight,
+ projectId,
+ discussionId,
+ mrUrl,
+ };
+}
+
+function logoutUser () {
+ const { localStorage } = window;
+
+ // All the browsers we support have localStorage, so let's silently fail
+ // and go on with the rest of the functionality.
+ try {
+ localStorage.removeItem('token');
+ } catch (err) {
+ return;
+ }
+
+ addLoginForm();
+}
+
+function postComment ({
+ href,
+ platform,
+ browser,
+ userAgent,
+ innerWidth,
+ innerHeight,
+ projectId,
+ discussionId,
+ mrUrl,
+}) {
+ // Clear any old errors
+ clearNote('gitlab-comment');
+
+ setInProgressState();
+
+ const commentText = document.getElementById('gitlab-comment').value.trim();
+
+ if (!commentText) {
+ postError('Your comment appears to be empty.', 'gitlab-comment');
+ resetCommentBox();
+ return;
+ }
+
+ const detailText = `
+ <details>
+ <summary>Metadata</summary>
+ Posted from ${href} | ${platform} | ${browser} | ${innerWidth} x ${innerHeight}.
+ <br /><br />
+ *User agent: ${userAgent}*
+ </details>
+ `;
+
+ const url = `
+ ${mrUrl}/api/v4/projects/${projectId}/issues/${discussionId}/discussions`;
+
+ const body = `${commentText}${detailText}`;
+
+ fetch(url, {
+ method: 'POST',
+ headers: {
+ 'PRIVATE-TOKEN': data.token,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ body }),
+ })
+ .then((response) => {
+ if (response.ok) {
+ confirmAndClear(discussionId);
+ return;
+ }
+
+ throw new Error(`${response.status}: ${response.statusText}`)
+ })
+ .catch((err) => {
+ postError(`The feedback was not sent successfully. Please try again. Error: ${err.message}`, 'gitlab-comment');
+ resetCommentBox();
+ });
+}
+
+function postError (message, inputId) {
+ const note = document.getElementById('gitlab-validation-note');
+ const field = document.getElementById(inputId);
+ field.style.borderColor = '#db3b21';
+ note.style.color = '#db3b21';
+ note.innerText = message;
+}
+
+function resetCommentBox() {
+ const commentBox = document.getElementById('gitlab-comment');
+ const commentButton = document.getElementById('gitlab-comment-button');
+
+ commentButton.innerText = 'Send feedback';
+ commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success');
+ commentButton.style.opacity = 1;
+
+ commentBox.style.pointerEvents = 'auto';
+ commentBox.style.color = 'rgba(0, 0, 0, 1)';
+}
+
+function resetCommentButton() {
+ const commentBox = document.getElementById('gitlab-comment');
+ const note = document.getElementById('gitlab-validation-note');
+
+ commentBox.value = '';
+ note.innerText = '';
+ resetCommentBox();
+}
+
+function setInProgressState() {
+ const commentButton = document.getElementById('gitlab-comment-button');
+ const commentBox = document.getElementById('gitlab-comment');
+
+ commentButton.innerText = 'Sending feedback';
+ commentButton.classList.replace('gitlab-button-success', 'gitlab-button-secondary');
+ commentButton.style.opacity = 0.5;
+ commentBox.style.color = 'rgba(223, 223, 223, 0.5)';
+ commentBox.style.pointerEvents = 'none';
+}
+
+function storeToken (token) {
+
+ const { localStorage } = window;
+
+ // All the browsers we support have localStorage, so let's silently fail
+ // and go on with the rest of the functionality.
+ try {
+ localStorage.setItem('token', token);
+ } catch (err) {
+ return;
+ }
+}
+
+function toggleForm () {
+ const container = document.getElementById('gitlab-review-container');
+ const collapseButton = document.getElementById('gitlab-collapse');
+ const form = document.getElementById('gitlab-form-wrapper');
+ const OPEN = 'open';
+ const CLOSED = 'closed';
+
+ const stateVals = {
+ [OPEN]: {
+ buttonClasses: ['gitlab-collapse-closed', 'gitlab-collapse-open'],
+ containerClasses: ['gitlab-closed-wrapper', 'gitlab-open-wrapper'],
+ icon: compressIcon,
+ display: 'flex',
+ backgroundColor: 'rgba(255, 255, 255, 1)',
+ },
+ [CLOSED]: {
+ buttonClasses: ['gitlab-collapse-open', 'gitlab-collapse-closed'],
+ containerClasses: ['gitlab-open-wrapper', 'gitlab-closed-wrapper'],
+ icon: commentIcon,
+ display: 'none',
+ backgroundColor: 'rgba(255, 255, 255, 0)',
+ },
+ }
+
+ const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
+
+ container.classList.replace(...stateVals[nextState].containerClasses);
+ container.style.backgroundColor = stateVals[nextState].backgroundColor;
+ form.style.display = stateVals[nextState].display;
+ collapseButton.classList.replace(...stateVals[nextState].buttonClasses);
+ collapseButton.innerHTML = stateVals[nextState].icon;
+}
+
+///////////////////////////////////////////////
+///////////////// INJECTION //////////////////
+///////////////////////////////////////////////
+
+function noop() {};
+
+const eventLookup = ({target: { id }}) => {
+ switch (id) {
+ case 'gitlab-collapse':
+ return toggleForm;
+ case 'gitlab-comment-button':
+ const projectDetails = getProjectDetails();
+ return postComment.bind(null, projectDetails);
+ case 'gitlab-login':
+ return authorizeUser;
+ case 'gitlab-logout-button':
+ return logoutUser;
+ default:
+ return noop;
+ }
+};
+
+window.addEventListener('load', () => {
+ const content = getInitialState();
+ const container = document.createElement('div');
+
+ container.setAttribute('id', 'gitlab-review-container');
+ container.insertAdjacentHTML('beforeend', collapseButton);
+ container.insertAdjacentHTML('beforeend', form(content));
+
+ document.body.insertBefore(container, document.body.firstChild);
+ addStylesheet();
+
+ document.getElementById('gitlab-review-container').addEventListener('click', (event) => {
+ eventLookup(event)();
+ });
+});
diff --git a/qa/README.md b/qa/README.md
index 8efdd8514f1..002ad4c65f5 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -49,8 +49,10 @@ will need to [modify your GDK setup](https://gitlab.com/gitlab-org/gitlab-qa/blo
### Writing tests
-1. [Using page objects](qa/page/README.md)
-2. [Style guide](STYLE_GUIDE.md)
+- [Writing tests from scratch tutorial](docs/WRITING_TESTS_FROM_SCRATCH.md)
+ - [Best practices](docs/BEST_PRACTICES.md)
+ - [Using page objects](qa/page/README.md)
+ - [Guidelines](docs/GUIDELINES.md)
### Running specific tests
diff --git a/qa/docs/BEST_PRACTICES.md b/qa/docs/BEST_PRACTICES.md
new file mode 100644
index 00000000000..3a2640607e4
--- /dev/null
+++ b/qa/docs/BEST_PRACTICES.md
@@ -0,0 +1,38 @@
+# Best practices when writing end-to-end tests
+
+The majority of the end-to-end tests require some state to be built in the application for the tests to happen.
+
+A good example is a user being logged in as a pre-condition for testing the feature.
+
+But if the login feature is already covered with end-to-end tests through the GUI, there is no reason to perform such an expensive task to test the functionality of creating a project, or importing a repo, even if this features depend on a user being logged in. Let's see an example to make things clear.
+
+Let's say that, on average, the process to perform a successful login through the GUI takes 2 seconds.
+
+Now, realize that almost all tests need the user to be logged in, and that we need every test to run in isolation, meaning that tests cannot interfere with each other. This would mean that for every test the user needs to log in, and "waste 2 seconds".
+
+Now, multiply the number of tests per 2 seconds, and as your test suite grows, the time to run it grows with it, and this is not sustainable.
+
+An alternative to perform a login in a cheaper way would be having an endpoint (available only for testing) where we could pass the user's credentials as encrypted values as query strings, and then we would be redirected to the logged in home page if the credentials are valid. Let's say that, on average, this process takes only 200 miliseconds.
+
+You see the point right?
+
+Performing a login through the GUI for every test would cost a lot in terms of tests' execution.
+
+And there is another reason.
+
+Let's say that you don't follow the above suggestion, and depend on the GUI for the creation of every application state in order to test a specific feature. In this case we could be talking about the **Issues** feature, that depends on a project to exist, and the user to be logged in.
+
+What would happen if there was a bug in the project creation page, where the 'Create' button is disabled, not allowing for the creation of a project through the GUI, but the API logic is still working?
+
+In this case, instead of having only the project creation test failing, we would have many tests that depend on a project to be failing too.
+
+But, if we were following the best practices, only one test would be failing, and tests for other features that depend on a project to exist would continue to pass, since they could be creating the project behind the scenes interacting directly with the public APIs, ensuring a more reliable metric of test failure rate.
+
+Finally, interacting with the application only by its GUI generates a higher rate of test flakiness, and we want to avoid that at max.
+
+**The takeaways here are:**
+
+- Building state through the GUI is time consuming and it's not sustainable as the test suite grows.
+- When depending only on the GUI to create the application's state and tests fail due to front-end issues, we can't rely on the test failures rate, and we generates a higher rate of test flakiness.
+
+Now that we are aware of all of it, [let's go create some tests](./WRITING_TESTS_FROM_SCRATCH.md).
diff --git a/qa/STYLE_GUIDE.md b/qa/docs/GUIDELINES.md
index 900f7456e1a..9db52cd07e6 100644
--- a/qa/STYLE_GUIDE.md
+++ b/qa/docs/GUIDELINES.md
@@ -43,4 +43,4 @@ end
Notice that in the above example, before clicking the `:operations_environments_link`, another element is hovered over.
-> We can create these methods as helpers to abstract multi-step navigation. \ No newline at end of file
+> We can create these methods as helpers to abstract multi-step navigation.
diff --git a/qa/docs/WRITING_TESTS_FROM_SCRATCH.md b/qa/docs/WRITING_TESTS_FROM_SCRATCH.md
new file mode 100644
index 00000000000..a6daffc964e
--- /dev/null
+++ b/qa/docs/WRITING_TESTS_FROM_SCRATCH.md
@@ -0,0 +1,380 @@
+# Writing end-to-end tests step-by-step
+
+In this tutorial, you will find different examples, and the steps involved, in the creation of end-to-end (_e2e_) tests for GitLab CE and GitLab EE, using GitLab QA.
+
+> When referring to end-to-end tests in this document, this means testing a specific feature end-to-end, such as a user logging in, the creation of a project, the management of labels, breaking down epics into sub-epics and issues, etc.
+
+## Important information before we start writing tests
+
+It's important to understand that end-to-end tests of isolated features, such as the ones described in the above note, doesn't mean that everything needs to happen through the GUI.
+
+If you don't exactly understand what we mean by **not everything needs to happen through the GUI,** please make sure you've read the [best practices](./BEST_PRACTICES.md) before moving on.
+
+## This document covers the following items:
+
+0. Identifying if end-to-end tests are really needed
+1. Identifying the [DevOps stage](https://about.gitlab.com/stages-devops-lifecycle/) of the feature that you are going to cover with end-to-end tests
+2. Creating the skeleton of the test file (`*_spec.rb`)
+3. The [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of the test cases logic
+4. Extracting duplicated code into methods
+5. Tests' pre-conditions (`before :all` and `before`) using resources and [Page Objects](./qa/page/README.md)
+6. Optimizing the test suite
+7. Using and implementing resources
+8. Moving elements definitions and its methods to [Page Objects](./qa/page/README.md)
+ - Adding testability to the application
+
+### 0. Are end-to-end tests needed?
+
+At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend to check the code coverage of a specific feature before writing end-to-end tests.
+
+Sometimes you may notice that there is already a good coverage in other test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
+
+If after this analysis you still think that end-to-end tests are needed, keep reading.
+
+### 1. Identifying the DevOps stage
+
+The GitLab QA end-to-end tests are organized by the different [stages in the DevOps lifecycle](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/specs/features/browser_ui), and so, if you are creating tests for issue creation, for instance, you would locate the spec files under the `qa/qa/specs/features/browser_ui/2_plan/` directory since issue creation is part of the Plan stage.
+
+ In another case of a test for listing merged merge requests (MRs), the test should go under the `qa/qa/specs/features/browser_ui/3_create/` directory since merge request is a feature from the Create stage.
+
+> There may be sub-directories inside the stages directories, for different features. For example: `.../browser_ui/2_plan/ee_epics/` and `.../browser_ui/2_plan/issues/`.
+
+Now, let's say we want to create tests for the [scoped labels](https://about.gitlab.com/2019/04/22/gitlab-11-10-released/#scoped-labels) feature, available on GitLab EE Premium (this feature is part of the Plan stage.)
+
+> Because these tests are for a feature available only on GitLab EE, we need to create them in the [EE repository](https://gitlab.com/gitlab-org/gitlab-ee).
+
+Since [there is no specific directory for this feature](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/qa/qa/specs/features/browser_ui/2_plan), we should create a sub-directory for it.
+
+Under `.../browser_ui/2_plan/`, let's create a sub-directory called `ee_scoped_labels/`.
+
+> Notice that since this feature is only available for GitLab EE we prefix the sub-directory with `ee_`.
+
+### 2. Test skeleton
+
+Inside the newly created sub-directory, let's create a file describing the test suite (e.g. `editing_scoped_labels_spec.rb`.)
+
+#### The `context` and `describe` blocks
+
+Specs have an outer `context` that indicates the DevOps stage. The next level is the `describe` block, that briefly states the subject of the test suite. See the following example:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels properties on issues' do
+ end
+ end
+end
+```
+
+#### The `it` blocks
+
+Every test suite is composed by at least one `it` block, and a good way to start writing end-to-end tests is by typing test cases descriptions as `it` blocks. Take a look at the following example:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels properties on issues' do
+ it 'replaces an existing label if it has the same key' do
+ end
+
+ it 'keeps both scoped labels when adding a label with a different key' do
+ end
+ end
+ end
+end
+```
+
+### 3. Test cases MVC
+
+For the [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of our test cases, let's say that we already have the application in the state needed for the tests, and then let's focus on the logic of the test cases only.
+
+To evolve the test cases drafted on step 2, let's imagine that the user is already logged in a GitLab EE instance, they already have at least a Premium license in use, there is already a project created, there is already an issue opened in the project, the issue already has a scoped label (e.g. `foo::bar`), there are other scoped labels (for the same scope and for a different scope, e.g. `foo::baz` and `bar::bah`), and finally, the user is already on the issue's page. Let's also suppose that for every test case the application is in a clean state, meaning that one test case won't affect another.
+
+> Note: there are different approaches to create an application state for end-to-end tests. Some of them are very time consuming and subject to failures, such as when using the GUI for all the pre-conditions of the tests. On the other hand, other approaches are more efficient, such as using the public APIs. The latter is more efficient since it doesn't depend on the GUI. We won't focus on this part yet, but it's good to keep it in mind.
+
+Let's now focus on the first test case.
+
+```ruby
+it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
+ # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects.
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['foo::baz', :enter]
+ page.find('#content-body').click
+ page.refresh
+
+ scoped_label = page.find('.qa-labels-block .scoped-label-wrapper')
+
+ expect(scoped_label).to have_content('foo::baz')
+ expect(scoped_label).not_to have_content('foo::bar')
+ expect(page).to have_content('added foo::baz label and removed foo::bar')
+end
+```
+
+> Notice that the test itself is simple. The most challenging part is the creation of the application state, which will be covered later.
+
+> The exemplified test cases' MVC is not enough for the change to be submitted in an MR, but they help on building up the test logic. The reason is that we do not want to use locators directly in the tests, and tests **must** use [Page Objects](./qa/page/README.md) before they can be merged.
+
+Below are the steps that the test covers:
+
+1. The test finds the 'Edit' link for the labels and clicks on it
+2. Then it fills in the 'Assign labels' input field with the value 'foo::baz' and press enter
+3. Then it clicks in the content body to apply the label and refreshes the page
+4. Finally the expectation that the previous scoped label was removed and that the new one was added happens
+
+Let's now see how the second test case would look like.
+
+```ruby
+it 'keeps both scoped labels when adding a label with a different key' do
+ # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects.
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['bar::bah', :enter]
+ page.find('#content-body').click
+ page.refresh
+
+ scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
+
+ expect(scoped_labels.first).to have_content('bar::bah')
+ expect(scoped_labels.last).to have_content('foo::ba')
+ expect(page).to have_content('added bar::bah')
+ expect(page).to have_content('added foo::ba')
+end
+```
+
+> Note that elements are always located using CSS selectors, and a good practice is to add test specific attribute:value for elements (this is called adding testability to the application and we will talk more about it later.)
+
+Below are the steps that the test covers:
+
+1. The test finds the 'Edit' link for the labels and clicks on it
+2. Then it fills in the 'Assign labels' input field with the value 'bar::bah' and press enter
+3. Then it clicks in the content body to apply the label and refreshes the page
+4. Finally the expectation that the both scoped labels are present happens
+
+> Similar to the previous test, this one is also very straight forward, but there is some code duplication. Let's address it.
+
+### 4. Extracting duplicated code
+
+If we refactor the tests created on step 3 we could come up with something like this:
+
+```ruby
+it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
+ select_label_and_refresh 'foo::baz'
+
+ expect(page).to have_content('added foo::baz')
+ expect(page).to have_content('and removed foo::bar')
+
+ scoped_label = page.find('.qa-labels-block .scoped-label-wrapper')
+
+ expect(scoped_label).to have_content('foo::baz')
+ expect(scoped_label).not_to have_content('foo::bar')
+end
+
+it 'keeps both scoped label when adding a label with a different key' do
+ select_label_and_refresh 'bar::bah'
+
+ expect(page).to have_content('added bar::bah')
+ expect(page).to have_content('added foo::ba')
+
+ scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
+
+ expect(scoped_labels.first).to have_content('bar::bah')
+ expect(scoped_labels.last).to have_content('foo::ba')
+end
+
+def select_label_and_refresh(label)
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
+ page.find('#content-body').click
+ page.refresh
+end
+```
+
+By creating a reusable `select_label_and_refresh` method we remove the code duplication, and later we can move this method to a Page Object class that will be created for easier maintenance purposes.
+
+> Notice that the reusable method is created in the bottom of the file. The reason for that is that reading the code should be similar to reading a newspaper, where high-level information is at the top, like the title and summary of the news, while low level, or more specific information, is at the bottom.
+
+### 5. Tests' pre-conditions using resources and Page Objects
+
+In this section, we will address the previously mentioned subject of creating the application state for the tests, using the `before :all` and `before` blocks, together with resources and Page Objects.
+
+#### `before :all`
+
+A pre-condition for the entire test suite is defined in the `before :all` block.
+
+For our test suite example, some things that could happen before the entire test suite starts are:
+
+- The user logging in;
+- A premium license already being set up;
+- A project being created with an issue and labels already setup.
+
+> In case of a test suite with only one `it` block it's ok to use only the `before` block (see below) with all the test's pre-conditions.
+
+#### `before`
+
+A pre-condition for each test case is defined in the `before` block.
+
+For our test cases samples, what we need is that for every test the issue page is opened, and there is only one scoped label applied to it.
+
+#### Implementation
+
+In the following code we will focus on the test suite and the test cases' pre-conditions only:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels properties on issues' do
+ before :all do
+ project = Resource::Project.fabricate_via_api! do |resource|
+ resource.name = 'scoped-labels-project'
+ end
+
+ @foo_bar_scoped_label = 'foo::bar'
+
+ @issue = Resource::Issue.fabricate_via_api! do |issue|
+ issue.project = project
+ issue.title = 'Issue to test the scoped labels'
+ issue.labels = @foo_bar_scoped_label
+ end
+
+ @labels = ['foo::baz', 'bar::bah']
+ @labels.each do |label|
+ Resource::Label.fabricate_via_api! do |l|
+ l.project = project.id
+ l.title = label
+ end
+ end
+
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ end
+
+ before do
+ Page::Project::Issue::Show.perform do |issue_page|
+ @issue.visit!
+ end
+ end
+
+ it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
+ ...
+ end
+
+ it 'keeps both scoped labels when adding a label with a different key' do
+ ...
+ end
+
+ def select_label_and_refresh(label)
+ ...
+ end
+ end
+ end
+end
+```
+
+In the `before :all` block we create all the application state needed for the tests to run. We do that by fabricating resources via APIs (`project`, `@issue`, and `@labels`), by using the `Runtime::Browser.visit` method to go to the login page, and by performing a `sign_in_using_credentials` from the `Login` Page Object.
+
+> When creating the resources, notice that when calling the `fabricate_via_api` method, we pass some attribute:values, like `name` for the `project` resoruce, `project`, `title`, and `labels` for the the issue resource, and `project`, and `title` for `label` resources.
+
+> What's important to understand here is that by creating the application state mostly using the public APIs we save a lot of time in the test suite setup stage.
+
+> Soon we will cover the use of the already existing resources' methods and the creation of your own `fabricate_via_api` methods for resources where this is still not available, but first, let's optimize our implementation.
+
+### 6. Optimization
+
+As already mentioned in the [best practices](./BEST_PRACTICES.md) document, end-to-end tests are very costly in terms of execution time, and it's our responsibility as software engineers to ensure that we optimize them as max as possible.
+
+> Differently than unit tests, that exercise every little piece of the application in isolation, usually having only one assertion per test, and being very fast to run, end-to-end tests can have more actions and assertions in a single test to help on speeding up the test's feedback since they are much slower when comparing to unit tests.
+
+Some improvements that we could make in our test suite to optimize its time to run are:
+
+1. Having a single test case (an `it` block) that exercise both scenarios to avoid "wasting" time in the tests' pre-conditions, instead of having two different test cases.
+2. Moving all the pre-conditions to the `before` block since there will be only one `it` block.
+3. Making the selection of labels more performant by allowing for the selection of more than one label in the same reusable method.
+
+Let's look at a suggestion that addresses the above points, one by one:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels properties on issues' do
+ before do
+ project = Resource::Project.fabricate_via_api! do |resource|
+ resource.name = 'scoped-labels-project'
+ end
+
+ @foo_bar_scoped_label = 'foo::bar'
+
+ @issue = Resource::Issue.fabricate_via_api! do |issue|
+ issue.project = project
+ issue.title = 'Issue to test the scoped labels'
+ issue.labels = @foo_bar_scoped_label
+ end
+
+ @labels = ['foo::baz', 'bar::bah']
+ @labels.each do |label|
+ Resource::Label.fabricate_via_api! do |l|
+ l.project = project.id
+ l.title = label
+ end
+ end
+
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ Page::Project::Issue::Show.perform do |issue_page|
+ @issue.visit!
+ end
+ end
+
+ it 'correctly applies the scoped labels depending if they are from the same or a different scope' do
+ select_labels_and_refresh @labels
+
+ scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
+
+ expect(page).to have_content("added #{@foo_bar_scoped_label}")
+ expect(page).to have_content("added #{@labels[1]} #{@labels[0]} labels and removed #{@foo_bar_scoped_label}")
+ expect(scoped_labels.count).to eq(2)
+ expect(scoped_labels.first).to have_content(@labels[1])
+ expect(scoped_labels.last).to have_content(@labels[0])
+ end
+
+ def select_labels_and_refresh(labels)
+ find('.block.labels .edit-link').click
+ labels.each do |label|
+ find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
+ end
+ find('#content-body').click
+ refresh
+ end
+ end
+ end
+ end
+```
+
+As you can see, now all the pre-conditions from the `before :all` block were moved to the `before` block, addressing point 2.
+
+To address point 1, we changed the test implementation from two `it` blocks into a single one that exercises both scenarios. Now the new test description is: `'correctly applies the scoped labels depending if they are from the same or a different scope'`. It's a long description, but it describes well what the test does.
+
+> Notice that the implementation of the new and unique `it` block had to change a little bit. Below we describe in details what it does.
+
+1. At the same time, it selects two scoped labels, one from the same scope of the one already applied in the issue during the setup phase (in the `before` block), and another one from a different scope.
+2. It runs the assertions that the labels where correctly added and removed; that only two labels are applied; and that those are the correct ones, and that they are shown in the right order.
+
+Finally, the `select_label_and_refresh` method is changed to `select_labels_and_refresh`, which accepts an array of labels instead of a single label, and it iterates on them for faster label selection (this is what is used in step 1 explained above.)
+
+### 7. Resources
+
+TBD.
+
+### 8. Page Objects
+
+> Page Objects are auto-loaded in the `qa/qa.rb` file and available in all the test files (`*_spec.rb`).
+
+Page Objects are used in end-to-end tests for maintenance reasons, where page's elements and methods are defined to be reused in any test.
+
+Take a look at [this document that specifically details the usage of Page Objects](./qa/page/README.md).
+
+Now, let's go back to our examples.
+
+...
+
+#### Adding testability
+
+TBD. \ No newline at end of file
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 9fabf83e2ce..c395e5f6011 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -113,8 +113,8 @@ module QA
has_css?(element_selector_css(name), wait: wait, text: text)
end
- def has_no_element?(name, wait: Capybara.default_max_wait_time)
- has_no_css?(element_selector_css(name), wait: wait)
+ def has_no_element?(name, text: nil, wait: Capybara.default_max_wait_time)
+ has_no_css?(element_selector_css(name), wait: wait, text: text)
end
def has_text?(text)
@@ -129,8 +129,8 @@ module QA
has_no_css?('.fa-spinner', wait: Capybara.default_max_wait_time)
end
- def within_element(name)
- page.within(element_selector_css(name)) do
+ def within_element(name, text: nil)
+ page.within(element_selector_css(name), text: text) do
yield
end
end
diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb
index 922a6ddb086..762eacdab15 100644
--- a/qa/qa/page/project/branches/show.rb
+++ b/qa/qa/page/project/branches/show.rb
@@ -7,6 +7,7 @@ module QA
class Show < Page::Base
view 'app/views/projects/branches/_branch.html.haml' do
element :remove_btn
+ element :branch_name
end
view 'app/views/projects/branches/_panel.html.haml' do
element :all_branches
@@ -27,11 +28,9 @@ module QA
finished_loading?
end
- def has_branch_title?(branch_title)
+ def has_no_branch?(branch_name)
within_element(:all_branches) do
- within(".item-title") do
- has_text?(branch_title)
- end
+ has_no_element?(:branch_name, text: branch_name, wait: Support::Waiter::DEFAULT_MAX_WAIT_TIME)
end
end
@@ -48,15 +47,6 @@ module QA
click_element(:delete_merged_branches)
end
end
-
- def wait_for_texts_not_to_be_visible(texts)
- text_not_visible = wait do
- texts.all? do |text|
- has_no_text?(text)
- end
- end
- raise "Expected text(s) #{texts} not to be visible" unless text_not_visible
- end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
index ac34f72bb8f..c0d597af076 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -5,7 +5,7 @@ module QA
describe 'filter issue comments activities' do
let(:issue_title) { 'issue title' }
- it 'user filters comments and activites in an issue' do
+ it 'user filters comments and activities in an issue' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
index c2c2b6da90a..cf6c24fa873 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
@@ -73,10 +73,9 @@ module QA
Page::Project::Branches::Show.perform do |branches_view|
branches_view.delete_branch(third_branch)
+ expect(branches_view).to have_no_branch(third_branch)
end
- expect(page).not_to have_content(third_branch)
-
Page::Project::Branches::Show.perform(&:delete_merged_branches)
expect(page).to have_content(
@@ -85,8 +84,7 @@ module QA
page.refresh
Page::Project::Branches::Show.perform do |branches_view|
- branches_view.wait_for_texts_not_to_be_visible([commit_message_of_second_branch])
- expect(branches_view).not_to have_branch_title(second_branch)
+ expect(branches_view).to have_no_branch(second_branch)
end
end
end
diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb
index 69b6332ecce..ff505fdbddd 100644
--- a/qa/qa/support/page/logging.rb
+++ b/qa/qa/support/page/logging.rb
@@ -76,23 +76,18 @@ module QA
super
end
- def has_element?(name, text: nil, wait: Capybara.default_max_wait_time)
+ def has_element?(name, **kwargs)
found = super
- msg = ["has_element? :#{name}"]
- msg << %Q(with text "#{text}") if text
- msg << "(wait: #{wait})"
- msg << "returned: #{found}"
-
- log(msg.compact.join(' '))
+ log_has_element_or_not('has_element?', name, found, **kwargs)
found
end
- def has_no_element?(name, wait: Capybara.default_max_wait_time)
+ def has_no_element?(name, **kwargs)
found = super
- log("has_no_element? :#{name} returned #{found}")
+ log_has_element_or_not('has_no_element?', name, found, **kwargs)
found
end
@@ -149,6 +144,15 @@ module QA
def log(msg)
QA::Runtime::Logger.debug(msg)
end
+
+ def log_has_element_or_not(method, name, found, **kwargs)
+ msg = ["#{method} :#{name}"]
+ msg << %Q(with text "#{kwargs[:text]}") if kwargs[:text]
+ msg << "(wait: #{kwargs[:wait] || Capybara.default_max_wait_time})"
+ msg << "returned: #{found}"
+
+ log(msg.compact.join(' '))
+ end
end
end
end
diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb
index 21a399b4a5f..fdcf2d7e157 100644
--- a/qa/qa/support/waiter.rb
+++ b/qa/qa/support/waiter.rb
@@ -3,9 +3,11 @@
module QA
module Support
module Waiter
+ DEFAULT_MAX_WAIT_TIME = 60
+
module_function
- def wait(max: 60, interval: 0.1)
+ def wait(max: DEFAULT_MAX_WAIT_TIME, interval: 0.1)
QA::Runtime::Logger.debug("with wait: max #{max}; interval #{interval}")
start = Time.now
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index 707a7ff6d98..99e96b81a51 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -93,7 +93,14 @@ describe QA::Support::Page::Logging do
allow(page).to receive(:has_no_css?).and_return(true)
expect { subject.has_no_element?(:element) }
- .to output(/has_no_element\? :element returned true/).to_stdout_from_any_process
+ .to output(/has_no_element\? :element \(wait: 2\) returned: true/).to_stdout_from_any_process
+ end
+
+ it 'logs has_no_element? with text' do
+ allow(page).to receive(:has_no_css?).and_return(true)
+
+ expect { subject.has_no_element?(:element, text: "more text") }
+ .to output(/has_no_element\? :element with text \"more text\" \(wait: 2\) returned: true/).to_stdout_from_any_process
end
it 'logs has_text?' do
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index 24ff1523ba7..a368ffba711 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -35,8 +35,10 @@ RSpec.configure do |config|
# show exception that triggers a retry if verbose_retry is set to true
config.display_try_failure_messages = true
- config.around do |example|
- retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 2
- example.run_with_retry retry: retry_times
+ if ENV['CI']
+ config.around do |example|
+ retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 2
+ example.run_with_retry retry: retry_times
+ end
end
end
diff --git a/scripts/clean-old-cached-assets b/scripts/clean-old-cached-assets
index 7a3a62a477a..8bdd3a9cdb6 100755
--- a/scripts/clean-old-cached-assets
+++ b/scripts/clean-old-cached-assets
@@ -1,6 +1,6 @@
#!/bin/bash
-# Clean up cached files that are older than 1 week
-find tmp/cache/assets/sprockets/ -type f -mtime +7 -execdir rm -- "{}" \;
+# Clean up cached files that are older than 4 days
+find tmp/cache/assets/sprockets/ -type f -mtime +4 -execdir rm -- "{}" \;
du -d 0 -h tmp/cache/assets/sprockets | cut -f1 | xargs -I % echo "tmp/cache/assets/sprockets/ is currently %"
diff --git a/scripts/frontend/stylelint/stylelint-utility-map.js b/scripts/frontend/stylelint/stylelint-utility-map.js
index 7e012b157b3..941198e82a4 100644
--- a/scripts/frontend/stylelint/stylelint-utility-map.js
+++ b/scripts/frontend/stylelint/stylelint-utility-map.js
@@ -29,7 +29,7 @@ sass.render(
// We just use postcss to create a CSS tree
postcss([])
.process(cssResult, {
- // This supresses a postcss warning
+ // This suppresses a postcss warning
from: undefined,
})
.then(result => {
diff --git a/spec/controllers/concerns/project_unauthorized_spec.rb b/spec/controllers/concerns/project_unauthorized_spec.rb
index 57ac00cf4dd..5834b1ef37f 100644
--- a/spec/controllers/concerns/project_unauthorized_spec.rb
+++ b/spec/controllers/concerns/project_unauthorized_spec.rb
@@ -12,7 +12,7 @@ describe ProjectUnauthorized do
render_views
- describe '#project_unauthorized_proc' do
+ describe '.on_routable_not_found' do
controller(::Projects::ApplicationController) do
def show
head :ok
diff --git a/spec/controllers/concerns/routable_actions_spec.rb b/spec/controllers/concerns/routable_actions_spec.rb
new file mode 100644
index 00000000000..59d48c68b9c
--- /dev/null
+++ b/spec/controllers/concerns/routable_actions_spec.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe RoutableActions do
+ controller(::ApplicationController) do
+ include RoutableActions # rubocop:disable RSpec/DescribedClass
+
+ before_action :routable
+
+ def routable
+ @klass = params[:type].constantize
+ @routable = find_routable!(params[:type].constantize, params[:id])
+ end
+
+ def show
+ head :ok
+ end
+ end
+
+ def get_routable(routable)
+ get :show, params: { id: routable.full_path, type: routable.class }
+ end
+
+ describe '#find_routable!' do
+ context 'when signed in' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'with a project' do
+ let(:routable) { create(:project) }
+
+ context 'when authorized' do
+ before do
+ routable.add_guest(user)
+ end
+
+ it 'returns the project' do
+ get_routable(routable)
+
+ expect(assigns[:routable]).to be_a(Project)
+ end
+
+ it 'allows access' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ it 'prevents access when not authorized' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'with a group' do
+ let(:routable) { create(:group, :private) }
+
+ context 'when authorized' do
+ before do
+ routable.add_guest(user)
+ end
+
+ it 'returns the group' do
+ get_routable(routable)
+
+ expect(assigns[:routable]).to be_a(Group)
+ end
+
+ it 'allows access' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ it 'prevents access when not authorized' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'with a user' do
+ let(:routable) { user }
+
+ it 'allows access when authorized' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'prevents access when unauthorized' do
+ allow(subject).to receive(:can?).and_return(false)
+
+ get_routable(user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to sign in for private resouces' do
+ routable = create(:project, :private)
+
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(response.location).to end_with('/users/sign_in')
+ end
+ end
+ end
+
+ describe '#perform_not_found_actions' do
+ let(:routable) { create(:project) }
+
+ before do
+ sign_in(create(:user))
+ end
+
+ it 'performs multiple checks' do
+ last_check_called = false
+ checks = [proc {}, proc { last_check_called = true }]
+ allow(subject).to receive(:not_found_actions).and_return(checks)
+
+ get_routable(routable)
+
+ expect(last_check_called).to eq(true)
+ end
+
+ it 'performs checks in the context of the controller' do
+ check = lambda { |routable| redirect_to routable }
+ allow(subject).to receive(:not_found_actions).and_return([check])
+
+ get_routable(routable)
+
+ expect(response.location).to end_with(routable.to_param)
+ end
+
+ it 'skips checks once one has resulted in a render/redirect' do
+ first_check = proc { render plain: 'first' }
+ second_check = proc { render plain: 'second' }
+ allow(subject).to receive(:not_found_actions).and_return([first_check, second_check])
+
+ get_routable(routable)
+
+ expect(response.body).to eq('first')
+ end
+ end
+end
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
index 8408578a7db..a3ce08f736c 100644
--- a/spec/controllers/concerns/send_file_upload_spec.rb
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
# frozen_string_literal: true
require 'spec_helper'
@@ -13,7 +14,7 @@ describe SendFileUpload do
# user/:id
def dynamic_segment
- File.join(model.class.to_s.underscore, model.id.to_s)
+ File.join(model.class.underscore, model.id.to_s)
end
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index cf23d937037..d5eea5b0439 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -383,6 +383,8 @@ describe Projects::EnvironmentsController do
end
describe 'GET #additional_metrics' do
+ let(:window_params) { { start: '1554702993.5398998', end: '1554717396.996232' } }
+
before do
allow(controller).to receive(:environment).and_return(environment)
end
@@ -394,7 +396,7 @@ describe Projects::EnvironmentsController do
context 'when requesting metrics as JSON' do
it 'returns a metrics JSON document' do
- additional_metrics
+ additional_metrics(window_params)
expect(response).to have_gitlab_http_status(204)
expect(json_response).to eq({})
@@ -414,23 +416,19 @@ describe Projects::EnvironmentsController do
end
it 'returns a metrics JSON document' do
- additional_metrics
+ additional_metrics(window_params)
expect(response).to be_ok
expect(json_response['success']).to be(true)
expect(json_response['data']).to eq({})
expect(json_response['last_update']).to eq(42)
end
+ end
- context 'when time params are provided' do
- it 'returns a metrics JSON document' do
- additional_metrics(start: '1554702993.5398998', end: '1554717396.996232')
-
- expect(response).to be_ok
- expect(json_response['success']).to be(true)
- expect(json_response['data']).to eq({})
- expect(json_response['last_update']).to eq(42)
- end
+ context 'when time params are missing' do
+ it 'raises an error when window params are missing' do
+ expect { additional_metrics }
+ .to raise_error(ActionController::ParameterMissing)
end
end
@@ -450,7 +448,7 @@ describe Projects::EnvironmentsController do
end
it 'raises an error when start is missing' do
- expect { additional_metrics(start: '1552647300.651094') }
+ expect { additional_metrics(end: '1552647300.651094') }
.to raise_error(ActionController::ParameterMissing)
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 088c515c3a6..9a598790ff2 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -46,13 +46,17 @@ describe RegistrationsController do
end
context 'when reCAPTCHA is enabled' do
+ def fail_recaptcha
+ # Without this, `verify_recaptcha` arbitrarily returns true in test env
+ Recaptcha.configuration.skip_verify_env.delete('test')
+ end
+
before do
stub_application_setting(recaptcha_enabled: true)
end
it 'displays an error when the reCAPTCHA is not solved' do
- # Without this, `verify_recaptcha` arbitrarily returns true in test env
- Recaptcha.configuration.skip_verify_env.delete('test')
+ fail_recaptcha
post(:create, params: user_params)
@@ -70,6 +74,17 @@ describe RegistrationsController do
expect(flash[:notice]).to include 'Welcome! You have signed up successfully.'
end
+
+ it 'does not require reCAPTCHA if disabled by feature flag' do
+ stub_feature_flags(registrations_recaptcha: false)
+ fail_recaptcha
+
+ post(:create, params: user_params)
+
+ expect(controller).not_to receive(:verify_recaptcha)
+ expect(flash[:alert]).to be_nil
+ expect(flash[:notice]).to include 'Welcome! You have signed up successfully.'
+ end
end
context 'when terms are enforced' do
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index 7256f785e1f..426abdc2a6c 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -13,7 +13,7 @@ FactoryBot.define do
end
# this needs to comply with RecordsUpload::Concern#upload_path
- path { File.join("uploads/-/system", model.class.to_s.underscore, mount_point.to_s, 'avatar.jpg') }
+ path { File.join("uploads/-/system", model.class.underscore, mount_point.to_s, 'avatar.jpg') }
trait :personal_snippet_upload do
uploader "PersonalFileUploader"
diff --git a/spec/features/admin/admin_sees_project_statistics_spec.rb b/spec/features/admin/admin_sees_project_statistics_spec.rb
new file mode 100644
index 00000000000..95d1fc5b57a
--- /dev/null
+++ b/spec/features/admin/admin_sees_project_statistics_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe "Admin > Admin sees project statistics" do
+ let(:current_user) { create(:admin) }
+
+ before do
+ sign_in(current_user)
+
+ visit admin_project_path(project)
+ end
+
+ context 'when project has statistics' do
+ let(:project) { create(:project, :repository) }
+
+ it "shows project statistics" do
+ expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes build artifacts, 0 Bytes LFS)")
+ end
+ end
+
+ context 'when project has no statistics' do
+ let(:project) { create(:project, :repository) { |project| project.statistics.destroy } }
+
+ it "shows 'Storage: Unknown'" do
+ expect(page).to have_content("Storage: Unknown")
+ end
+ end
+end
diff --git a/spec/features/admin/admin_sees_projects_statistics_spec.rb b/spec/features/admin/admin_sees_projects_statistics_spec.rb
new file mode 100644
index 00000000000..6a6f369ac7c
--- /dev/null
+++ b/spec/features/admin/admin_sees_projects_statistics_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe "Admin > Admin sees projects statistics" do
+ let(:current_user) { create(:admin) }
+
+ before do
+ create(:project, :repository)
+ create(:project, :repository) { |project| project.statistics.destroy }
+
+ sign_in(current_user)
+
+ visit admin_projects_path
+ end
+
+ it "shows project statistics for projects that have them" do
+ expect(page.all('.stats').map(&:text)).to contain_exactly("0 Bytes", "Unknown")
+ end
+end
diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
index b6dbf76bc9b..51c884201a6 100644
--- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -5,6 +5,8 @@ describe 'Projects > Files > User views files page' do
let(:user) { project.owner }
before do
+ stub_feature_flags(vue_file_list: false)
+
sign_in user
visit project_tree_path(project, project.repository.root_ref)
end
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 66268355345..a5d849db8a3 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -11,6 +11,7 @@ describe "User browses files" do
let(:user) { project.owner }
before do
+ stub_feature_flags(vue_file_list: false)
stub_feature_flags(csslab: false)
sign_in(user)
end
diff --git a/spec/features/projects/files/user_browses_lfs_files_spec.rb b/spec/features/projects/files/user_browses_lfs_files_spec.rb
index d56476adb05..d5cb8f9212d 100644
--- a/spec/features/projects/files/user_browses_lfs_files_spec.rb
+++ b/spec/features/projects/files/user_browses_lfs_files_spec.rb
@@ -5,6 +5,8 @@ describe 'Projects > Files > User browses LFS files' do
let(:user) { project.owner }
before do
+ stub_feature_flags(vue_file_list: false)
+
sign_in(user)
end
diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb
index 847b5f0860f..e29e867492e 100644
--- a/spec/features/projects/files/user_creates_directory_spec.rb
+++ b/spec/features/projects/files/user_creates_directory_spec.rb
@@ -11,6 +11,8 @@ describe 'Projects > Files > User creates a directory', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_file_list: false)
+
project.add_developer(user)
sign_in(user)
visit project_tree_path(project, 'master')
diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb
index 614b11fa5c8..11ee87f245b 100644
--- a/spec/features/projects/files/user_deletes_files_spec.rb
+++ b/spec/features/projects/files/user_deletes_files_spec.rb
@@ -12,6 +12,8 @@ describe 'Projects > Files > User deletes files', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_file_list: false)
+
sign_in(user)
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 2de22582b2c..26efb5e6787 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -10,6 +10,7 @@ describe 'Projects > Files > User edits files', :js do
before do
stub_feature_flags(web_ide_default: false)
+ stub_feature_flags(vue_file_list: false)
sign_in(user)
end
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index e3da28d73c3..bfd612e4cc8 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -14,6 +14,8 @@ describe 'Projects > Files > User replaces files', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_file_list: false)
+
sign_in(user)
end
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index af3fc528a20..25ff3fdf411 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -14,6 +14,8 @@ describe 'Projects > Files > User uploads files' do
let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
before do
+ stub_feature_flags(vue_file_list: false)
+
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index 45e81e1c040..3ccea2db705 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -8,6 +8,7 @@ describe 'Projects tree', :js do
let(:test_sha) { '7975be0116940bf2ad4321f79d02a55c5f7779aa' }
before do
+ stub_feature_flags(vue_file_list: false)
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index ff4e6197746..4fe45311b2d 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -5,6 +5,7 @@ describe 'Project' do
include MobileHelpers
before do
+ stub_feature_flags(vue_file_list: false)
stub_feature_flags(approval_rules: false)
end
diff --git a/spec/fixtures/api/schemas/entities/test_case.json b/spec/fixtures/api/schemas/entities/test_case.json
index c9ba1f3ad18..70f6edeeeb7 100644
--- a/spec/fixtures/api/schemas/entities/test_case.json
+++ b/spec/fixtures/api/schemas/entities/test_case.json
@@ -7,6 +7,7 @@
"properties": {
"status": { "type": "string" },
"name": { "type": "string" },
+ "classname": { "type": "string" },
"execution_time": { "type": "float" },
"system_output": { "type": ["string", "null"] },
"stack_trace": { "type": ["string", "null"] }
diff --git a/spec/fixtures/api/schemas/public_api/v4/label_basic.json b/spec/fixtures/api/schemas/public_api/v4/label_basic.json
new file mode 100644
index 00000000000..37bbdcb14fe
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/label_basic.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "color",
+ "description",
+ "text_color"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+ },
+ "description": { "type": ["string", "null"] },
+ "text_color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
new file mode 100644
index 00000000000..d23393db60d
--- /dev/null
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -0,0 +1,67 @@
+import actions from '~/boards/stores/actions';
+
+const expectNotImplemented = action => {
+ it('is not implemented', () => {
+ expect(action).toThrow(new Error('Not implemented!'));
+ });
+};
+
+describe('setEndpoints', () => {
+ expectNotImplemented(actions.setEndpoints);
+});
+
+describe('fetchLists', () => {
+ expectNotImplemented(actions.fetchLists);
+});
+
+describe('generateDefaultLists', () => {
+ expectNotImplemented(actions.generateDefaultLists);
+});
+
+describe('createList', () => {
+ expectNotImplemented(actions.createList);
+});
+
+describe('updateList', () => {
+ expectNotImplemented(actions.updateList);
+});
+
+describe('deleteList', () => {
+ expectNotImplemented(actions.deleteList);
+});
+
+describe('fetchIssuesForList', () => {
+ expectNotImplemented(actions.fetchIssuesForList);
+});
+
+describe('moveIssue', () => {
+ expectNotImplemented(actions.moveIssue);
+});
+
+describe('createNewIssue', () => {
+ expectNotImplemented(actions.createNewIssue);
+});
+
+describe('fetchBacklog', () => {
+ expectNotImplemented(actions.fetchBacklog);
+});
+
+describe('bulkUpdateIssues', () => {
+ expectNotImplemented(actions.bulkUpdateIssues);
+});
+
+describe('fetchIssue', () => {
+ expectNotImplemented(actions.fetchIssue);
+});
+
+describe('toggleIssueSubscription', () => {
+ expectNotImplemented(actions.toggleIssueSubscription);
+});
+
+describe('showPage', () => {
+ expectNotImplemented(actions.showPage);
+});
+
+describe('toggleEmptyState', () => {
+ expectNotImplemented(actions.toggleEmptyState);
+});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
new file mode 100644
index 00000000000..aa477766978
--- /dev/null
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -0,0 +1,91 @@
+import mutations from '~/boards/stores/mutations';
+
+const expectNotImplemented = action => {
+ it('is not implemented', () => {
+ expect(action).toThrow(new Error('Not implemented!'));
+ });
+};
+
+describe('SET_ENDPOINTS', () => {
+ expectNotImplemented(mutations.SET_ENDPOINTS);
+});
+
+describe('REQUEST_ADD_LIST', () => {
+ expectNotImplemented(mutations.REQUEST_ADD_LIST);
+});
+
+describe('RECEIVE_ADD_LIST_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_ADD_LIST_SUCCESS);
+});
+
+describe('RECEIVE_ADD_LIST_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_ADD_LIST_ERROR);
+});
+
+describe('REQUEST_UPDATE_LIST', () => {
+ expectNotImplemented(mutations.REQUEST_UPDATE_LIST);
+});
+
+describe('RECEIVE_UPDATE_LIST_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_UPDATE_LIST_SUCCESS);
+});
+
+describe('RECEIVE_UPDATE_LIST_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_UPDATE_LIST_ERROR);
+});
+
+describe('REQUEST_REMOVE_LIST', () => {
+ expectNotImplemented(mutations.REQUEST_REMOVE_LIST);
+});
+
+describe('RECEIVE_REMOVE_LIST_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_SUCCESS);
+});
+
+describe('RECEIVE_REMOVE_LIST_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR);
+});
+
+describe('REQUEST_ADD_ISSUE', () => {
+ expectNotImplemented(mutations.REQUEST_ADD_ISSUE);
+});
+
+describe('RECEIVE_ADD_ISSUE_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_SUCCESS);
+});
+
+describe('RECEIVE_ADD_ISSUE_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_ERROR);
+});
+
+describe('REQUEST_MOVE_ISSUE', () => {
+ expectNotImplemented(mutations.REQUEST_MOVE_ISSUE);
+});
+
+describe('RECEIVE_MOVE_ISSUE_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_MOVE_ISSUE_SUCCESS);
+});
+
+describe('RECEIVE_MOVE_ISSUE_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_MOVE_ISSUE_ERROR);
+});
+
+describe('REQUEST_UPDATE_ISSUE', () => {
+ expectNotImplemented(mutations.REQUEST_UPDATE_ISSUE);
+});
+
+describe('RECEIVE_UPDATE_ISSUE_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_SUCCESS);
+});
+
+describe('RECEIVE_UPDATE_ISSUE_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR);
+});
+
+describe('SET_CURRENT_PAGE', () => {
+ expectNotImplemented(mutations.SET_CURRENT_PAGE);
+});
+
+describe('TOGGLE_EMPTY_STATE', () => {
+ expectNotImplemented(mutations.TOGGLE_EMPTY_STATE);
+});
diff --git a/spec/frontend/boards/stores/state_spec.js b/spec/frontend/boards/stores/state_spec.js
new file mode 100644
index 00000000000..35490a63567
--- /dev/null
+++ b/spec/frontend/boards/stores/state_spec.js
@@ -0,0 +1,11 @@
+import createState from '~/boards/stores/state';
+
+describe('createState', () => {
+ it('is a function', () => {
+ expect(createState).toEqual(expect.any(Function));
+ });
+
+ it('returns an object', () => {
+ expect(createState()).toEqual(expect.any(Object));
+ });
+});
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 9612162ad0c..a8c8688441d 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -38,6 +38,16 @@ class CustomEnvironment extends JSDOMEnvironment {
this.global.fixturesBasePath = `${process.cwd()}/${
IS_EE ? 'ee/' : ''
}spec/javascripts/fixtures`;
+
+ // Not yet supported by JSDOM: https://github.com/jsdom/jsdom/issues/317
+ this.global.document.createRange = () => ({
+ setStart: () => {},
+ setEnd: () => {},
+ commonAncestorContainer: {
+ nodeName: 'BODY',
+ ownerDocument: this.global.document,
+ },
+ });
}
async teardown() {
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 3886853f3c1..8af49fd47a2 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -7,11 +7,8 @@ import 'jquery.caret';
import 'at.js';
import { TEST_HOST } from 'helpers/test_constants';
-import { setTestTimeout } from 'helpers/timeout';
import { getJSONFixture } from 'helpers/fixtures';
-setTestTimeout(500);
-
const labelsFixture = getJSONFixture('autocomplete_sources/labels.json');
describe('GfmAutoComplete', () => {
diff --git a/spec/frontend/helpers/timeout.js b/spec/frontend/helpers/timeout.js
index 318593a48a4..b30b7f1ce1e 100644
--- a/spec/frontend/helpers/timeout.js
+++ b/spec/frontend/helpers/timeout.js
@@ -1,24 +1,31 @@
-let testTimeoutInMs;
+const NS_PER_SEC = 1e9;
+const NS_PER_MS = 1e6;
-export const setTestTimeout = newTimeoutInMs => {
- testTimeoutInMs = newTimeoutInMs;
- jest.setTimeout(newTimeoutInMs);
+let testTimeoutNS;
+
+export const setTestTimeout = newTimeoutMS => {
+ testTimeoutNS = newTimeoutMS * NS_PER_MS;
+ jest.setTimeout(newTimeoutMS);
};
-export const initializeTestTimeout = defaultTimeoutInMs => {
- setTestTimeout(defaultTimeoutInMs);
+export const initializeTestTimeout = defaultTimeoutMS => {
+ setTestTimeout(defaultTimeoutMS);
let testStartTime;
// https://github.com/facebook/jest/issues/6947
beforeEach(() => {
- testStartTime = Date.now();
+ testStartTime = process.hrtime();
});
afterEach(() => {
- const elapsedTimeInMs = Date.now() - testStartTime;
- if (elapsedTimeInMs > testTimeoutInMs) {
- throw new Error(`Test took too long (${elapsedTimeInMs}ms > ${testTimeoutInMs}ms)!`);
+ const [seconds, remainingNs] = process.hrtime(testStartTime);
+ const elapsedNS = seconds * NS_PER_SEC + remainingNs;
+
+ if (elapsedNS > testTimeoutNS) {
+ throw new Error(
+ `Test took too long (${elapsedNS / NS_PER_MS}ms > ${testTimeoutNS / NS_PER_MS}ms)!`,
+ );
}
});
};
diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 0878c1de095..9e920d59093 100644
--- a/spec/frontend/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -155,11 +155,11 @@ describe('text_utility', () => {
expect(textUtils.truncateNamespace('a / b')).toBe('a');
});
- it(`should return the first 2 namespaces if the namespace inlcudes exactly 2 levels`, () => {
+ it(`should return the first 2 namespaces if the namespace includes exactly 2 levels`, () => {
expect(textUtils.truncateNamespace('a / b / c')).toBe('a / b');
});
- it(`should return the first and last namespaces, separated by "...", if the namespace inlcudes more than 2 levels`, () => {
+ it(`should return the first and last namespaces, separated by "...", if the namespace includes more than 2 levels`, () => {
expect(textUtils.truncateNamespace('a / b / c / d')).toBe('a / ... / c');
expect(textUtils.truncateNamespace('a / b / c / d / e / f / g / h / i')).toBe('a / ... / h');
});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 7e037beca9d..c3204b3aaa0 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -8,7 +8,6 @@ import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_sys
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import createStore from '~/notes/stores';
-import { setTestTimeout } from 'helpers/timeout';
import {
noteableDataMock,
discussionMock,
@@ -18,8 +17,6 @@ import {
const localVue = createLocalVue();
describe('DiscussionNotes', () => {
- setTestTimeout(500);
-
let wrapper;
const createComponent = props => {
diff --git a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
index f163bdd9913..7e9aec84016 100644
--- a/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
+++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
@@ -1,9 +1,6 @@
import $ from 'jquery';
import '~/lib/utils/text_utility';
import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports';
-import { setTestTimeout } from 'helpers/timeout';
-
-setTestTimeout(500);
describe('Abuse Reports', () => {
const FIXTURE = 'abuse_reports/abuse_reports_list.html';
diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/reports/components/report_item_spec.js
new file mode 100644
index 00000000000..bacbb399513
--- /dev/null
+++ b/spec/frontend/reports/components/report_item_spec.js
@@ -0,0 +1,33 @@
+import { shallowMount } from '@vue/test-utils';
+import { STATUS_SUCCESS } from '~/reports/constants';
+import ReportItem from '~/reports/components/report_item.vue';
+import { componentNames } from '~/reports/components/issue_body';
+
+describe('ReportItem', () => {
+ describe('showReportSectionStatusIcon', () => {
+ it('does not render CI Status Icon when showReportSectionStatusIcon is false', () => {
+ const wrapper = shallowMount(ReportItem, {
+ propsData: {
+ issue: { foo: 'bar' },
+ component: componentNames.TestIssueBody,
+ status: STATUS_SUCCESS,
+ showReportSectionStatusIcon: false,
+ },
+ });
+
+ expect(wrapper.find('issuestatusicon-stub').exists()).toBe(false);
+ });
+
+ it('shows status icon when unspecified', () => {
+ const wrapper = shallowMount(ReportItem, {
+ propsData: {
+ issue: { foo: 'bar' },
+ component: componentNames.TestIssueBody,
+ status: STATUS_SUCCESS,
+ },
+ });
+
+ expect(wrapper.find('issuestatusicon-stub').exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js
new file mode 100644
index 00000000000..f61a0ccd1e6
--- /dev/null
+++ b/spec/frontend/repository/router_spec.js
@@ -0,0 +1,23 @@
+import IndexPage from '~/repository/pages/index.vue';
+import TreePage from '~/repository/pages/tree.vue';
+import createRouter from '~/repository/router';
+
+describe('Repository router spec', () => {
+ it.each`
+ path | component | componentName
+ ${'/'} | ${IndexPage} | ${'IndexPage'}
+ ${'/tree/master'} | ${TreePage} | ${'TreePage'}
+ ${'/tree/master/app/assets'} | ${TreePage} | ${'TreePage'}
+ ${'/tree/123/app/assets'} | ${null} | ${'null'}
+ `('sets component as $componentName for path "$path"', ({ path, component }) => {
+ const router = createRouter('', 'master');
+
+ const componentsForRoute = router.getMatchedComponents(path);
+
+ expect(componentsForRoute.length).toBe(component ? 1 : 0);
+
+ if (component) {
+ expect(componentsForRoute).toContain(component);
+ }
+ });
+});
diff --git a/spec/frontend/serverless/components/function_row_spec.js b/spec/frontend/serverless/components/function_row_spec.js
index 414fdc5cd82..979f98c4832 100644
--- a/spec/frontend/serverless/components/function_row_spec.js
+++ b/spec/frontend/serverless/components/function_row_spec.js
@@ -1,27 +1,32 @@
import functionRowComponent from '~/serverless/components/function_row.vue';
import { shallowMount } from '@vue/test-utils';
+import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockServerlessFunction } from '../mock_data';
-const createComponent = func =>
- shallowMount(functionRowComponent, { propsData: { func }, sync: false }).vm;
-
describe('functionRowComponent', () => {
- it('Parses the function details correctly', () => {
- const vm = createComponent(mockServerlessFunction);
+ let wrapper;
- expect(vm.$el.querySelector('b').innerHTML).toEqual(mockServerlessFunction.name);
- expect(vm.$el.querySelector('span').innerHTML).toEqual(mockServerlessFunction.image);
- expect(vm.$el.querySelector('timeago-stub').getAttribute('time')).not.toBe(null);
+ const createComponent = func => {
+ wrapper = shallowMount(functionRowComponent, { propsData: { func }, sync: false });
+ };
- vm.$destroy();
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Parses the function details correctly', () => {
+ createComponent(mockServerlessFunction);
+
+ expect(wrapper.find('b').text()).toBe(mockServerlessFunction.name);
+ expect(wrapper.find('span').text()).toBe(mockServerlessFunction.image);
+ expect(wrapper.find(Timeago).attributes('time')).not.toBe(null);
});
it('handles clicks correctly', () => {
- const vm = createComponent(mockServerlessFunction);
+ createComponent(mockServerlessFunction);
+ const { vm } = wrapper;
expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row
-
- vm.$destroy();
});
});
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
index 7af33ceaadc..6924fb9e91f 100644
--- a/spec/frontend/serverless/components/functions_spec.js
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -1,9 +1,12 @@
import Vuex from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import functionsComponent from '~/serverless/components/functions.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { createStore } from '~/serverless/store';
+import EmptyState from '~/serverless/components/empty_state.vue';
+import EnvironmentRow from '~/serverless/components/environment_row.vue';
import { TEST_HOST } from 'helpers/test_constants';
import { mockServerlessFunctions } from '../mock_data';
@@ -43,7 +46,7 @@ describe('functionsComponent', () => {
sync: false,
});
- expect(component.vm.$el.querySelector('emptystate-stub')).not.toBe(null);
+ expect(component.find(EmptyState).exists()).toBe(true);
});
it('should render a loading component', () => {
@@ -60,7 +63,7 @@ describe('functionsComponent', () => {
sync: false,
});
- expect(component.vm.$el.querySelector('glloadingicon-stub')).not.toBe(null);
+ expect(component.find(GlLoadingIcon).exists()).toBe(true);
});
it('should render empty state when there is no function data', () => {
@@ -104,7 +107,7 @@ describe('functionsComponent', () => {
component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
return component.vm.$nextTick().then(() => {
- expect(component.vm.$el.querySelector('environmentrow-stub')).not.toBe(null);
+ expect(component.find(EnvironmentRow).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/serverless/components/missing_prometheus_spec.js b/spec/frontend/serverless/components/missing_prometheus_spec.js
index d0df6125290..5dbdccde2de 100644
--- a/spec/frontend/serverless/components/missing_prometheus_spec.js
+++ b/spec/frontend/serverless/components/missing_prometheus_spec.js
@@ -1,3 +1,4 @@
+import { GlButton } from '@gitlab/ui';
import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue';
import { shallowMount } from '@vue/test-utils';
@@ -9,27 +10,29 @@ const createComponent = missingData =>
missingData,
},
sync: false,
- }).vm;
+ });
describe('missingPrometheusComponent', () => {
- let vm;
+ let wrapper;
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('should render missing prometheus message', () => {
- vm = createComponent(false);
+ wrapper = createComponent(false);
+ const { vm } = wrapper;
expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
'Function invocation metrics require Prometheus to be installed first.',
);
- expect(vm.$el.querySelector('glbutton-stub').getAttribute('variant')).toEqual('success');
+ expect(wrapper.find(GlButton).attributes('variant')).toBe('success');
});
it('should render no prometheus data message', () => {
- vm = createComponent(true);
+ wrapper = createComponent(true);
+ const { vm } = wrapper;
expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
'Invocation metrics loading or not available at this time.',
diff --git a/spec/frontend/serverless/components/url_spec.js b/spec/frontend/serverless/components/url_spec.js
index d05a9bba103..706441e8a8b 100644
--- a/spec/frontend/serverless/components/url_spec.js
+++ b/spec/frontend/serverless/components/url_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import urlComponent from '~/serverless/components/url.vue';
import { shallowMount } from '@vue/test-utils';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
const createComponent = uri =>
shallowMount(Vue.extend(urlComponent), {
@@ -8,15 +9,16 @@ const createComponent = uri =>
uri,
},
sync: false,
- }).vm;
+ });
describe('urlComponent', () => {
it('should render correctly', () => {
const uri = 'http://testfunc.apps.example.com';
- const vm = createComponent(uri);
+ const wrapper = createComponent(uri);
+ const { vm } = wrapper;
expect(vm.$el.classList.contains('clipboard-group')).toBe(true);
- expect(vm.$el.querySelector('clipboardbutton-stub').getAttribute('text')).toEqual(uri);
+ expect(wrapper.find(ClipboardButton).attributes('text')).toEqual(uri);
expect(vm.$el.querySelector('.url-text-field').innerHTML).toEqual(uri);
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index c57e0e7cfc6..c24f0bc4776 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -3,7 +3,7 @@ import * as jqueryMatchers from 'custom-jquery-matchers';
import Translate from '~/vue_shared/translate';
import axios from '~/lib/utils/axios_utils';
import { initializeTestTimeout } from './helpers/timeout';
-import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './helpers/fixtures';
+import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures';
process.on('unhandledRejection', global.promiseRejectionHandler);
@@ -15,7 +15,7 @@ afterEach(() =>
}),
);
-initializeTestTimeout(300);
+initializeTestTimeout(500);
// fail tests for unmocked requests
beforeEach(done => {
@@ -46,9 +46,12 @@ Object.defineProperty(global.Element.prototype, 'innerText', {
// convenience wrapper for migration from Karma
Object.assign(global, {
loadFixtures: loadHTMLFixture,
- loadJSONFixtures: getJSONFixture,
- preloadFixtures() {},
setFixtures: setHTMLFixture,
+
+ // The following functions fill the fixtures cache in Karma.
+ // This is not necessary in Jest because we make no Ajax request.
+ loadJSONFixtures() {},
+ preloadFixtures() {},
});
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
diff --git a/spec/helpers/dashboard_helper_spec.rb b/spec/helpers/dashboard_helper_spec.rb
index 7ba24ba2956..023238ee0ae 100644
--- a/spec/helpers/dashboard_helper_spec.rb
+++ b/spec/helpers/dashboard_helper_spec.rb
@@ -21,4 +21,10 @@ describe DashboardHelper do
expect(helper.dashboard_nav_links).not_to include(:activity, :milestones)
end
end
+
+ describe '.has_start_trial?' do
+ subject { helper.has_start_trial? }
+
+ it { is_expected.to eq(false) }
+ end
end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 03b4c19ec22..0434af25866 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -1,6 +1,45 @@
require 'spec_helper'
describe EmailsHelper do
+ describe 'closure_reason_text' do
+ context 'when given a MergeRequest' do
+ let(:merge_request) { create(:merge_request) }
+ let(:merge_request_presenter) { merge_request.present }
+
+ context "and format is text" do
+ it "returns plain text" do
+ expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
+ end
+ end
+
+ context "and format is HTML" do
+ it "returns HTML" do
+ expect(closure_reason_text(merge_request, format: :html)).to eq(" via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}")
+ end
+ end
+
+ context "and format is unknown" do
+ it "returns plain text" do
+ expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
+ end
+ end
+ end
+
+ context 'when given a String' do
+ let(:closed_via) { "5a0eb6fd7e0f133044378c662fcbbc0d0c16dbfa" }
+
+ it "returns plain text" do
+ expect(closure_reason_text(closed_via)).to eq(" via #{closed_via}")
+ end
+ end
+
+ context 'when not given anything' do
+ it "returns empty string" do
+ expect(closure_reason_text(nil)).to eq("")
+ end
+ end
+ end
+
describe 'sanitize_name' do
context 'when name contains a valid URL string' do
it 'returns name with `.` replaced with `_` to prevent mail clients from auto-linking URLs' do
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index e840c927d59..979d89812f5 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -50,4 +50,16 @@ describe NavHelper do
expect(helper.header_links).to contain_exactly(:sign_in, :search)
end
end
+
+ context '.admin_monitoring_nav_links' do
+ subject { helper.admin_monitoring_nav_links }
+
+ it { is_expected.to all(be_a(String)) }
+ end
+
+ context '.group_issues_sub_menu_items' do
+ subject { helper.group_issues_sub_menu_items }
+
+ it { is_expected.to all(be_a(String)) }
+ end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 554cb861563..83271aa24a3 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -799,4 +799,24 @@ describe ProjectsHelper do
it { is_expected.to eq(result) }
end
end
+
+ describe '#can_import_members?' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:owner) { project.owner }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ it 'returns false if user cannot admin_project_member' do
+ allow(helper).to receive(:current_user) { user }
+ expect(helper.can_import_members?).to eq false
+ end
+
+ it 'returns true if user can admin_project_member' do
+ allow(helper).to receive(:current_user) { owner }
+ expect(helper.can_import_members?).to eq true
+ end
+ end
end
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index c82dcadd2f1..6309a8823d7 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -82,7 +82,7 @@ describe('DiffsStoreActions', () => {
describe('fetchDiffFiles', () => {
it('should fetch diff files', done => {
- const endpoint = '/fetch/diff/files';
+ const endpoint = '/fetch/diff/files?w=1';
const mock = new MockAdapter(axios);
const res = { diff_files: 1, merge_request_diffs: [] };
mock.onGet(endpoint).reply(200, res);
@@ -828,6 +828,10 @@ describe('DiffsStoreActions', () => {
});
describe('setShowWhitespace', () => {
+ beforeEach(() => {
+ spyOn(eventHub, '$emit').and.stub();
+ });
+
it('commits SET_SHOW_WHITESPACE', done => {
testAction(
setShowWhitespace,
@@ -855,6 +859,30 @@ describe('DiffsStoreActions', () => {
expect(window.history.pushState).toHaveBeenCalled();
});
+
+ it('calls history pushState with merged params', () => {
+ const originalPushState = window.history;
+
+ originalPushState.pushState({}, '', '?test=1');
+
+ spyOn(localStorage, 'setItem').and.stub();
+ spyOn(window.history, 'pushState').and.stub();
+
+ setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true });
+
+ expect(window.history.pushState.calls.mostRecent().args[2]).toMatch(/(.*)\?test=1&w=0/);
+
+ originalPushState.pushState({}, '', '?');
+ });
+
+ it('emits eventHub event', () => {
+ spyOn(localStorage, 'setItem').and.stub();
+ spyOn(window.history, 'pushState').and.stub();
+
+ setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true });
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('refetchDiffData');
+ });
});
describe('setRenderIt', () => {
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index 7ef44f29c5b..4772f754937 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -14,6 +14,7 @@ import {
setSidebar,
subItemsMouseLeave,
} from '~/fly_out_nav';
+import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
import bp from '~/breakpoints';
describe('Fly out sidebar navigation', () => {
@@ -219,7 +220,7 @@ describe('Fly out sidebar navigation', () => {
it('shows collapsed only sub-items if icon only sidebar', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-collapsed-desktop');
+ sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
subItems.classList.add('is-fly-out-only');
setSidebar(sidebar);
@@ -296,7 +297,7 @@ describe('Fly out sidebar navigation', () => {
it('returns true when active & collapsed sidebar', () => {
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-collapsed-desktop');
+ sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
el.classList.add('active');
setSidebar(sidebar);
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
index 34c0cd435cd..3ba0033171e 100644
--- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
+++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
@@ -99,7 +99,7 @@ describe('ProjectSelector component', () => {
expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search');
});
- it(`shows a "minimum seach query" message if showMinimumSearchQueryMessage === true`, () => {
+ it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, () => {
wrapper.setProps({ showMinimumSearchQueryMessage: true });
expect(wrapper.contains('.js-minimum-search-query-message')).toBe(true);
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 05057789cc1..80ca7a63435 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -80,7 +80,7 @@ describe Banzai::Filter::SyntaxHighlightFilter do
let(:lang) { 'suggestion' }
let(:lang_params) { '-1+10' }
- it "delimits on the first appearence" do
+ it "delimits on the first appearance" do
result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>})
expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>})
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 2a3f7807fdb..491e3fba9d9 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -174,6 +174,13 @@ describe Gitlab::Ci::CronParser do
it { expect(subject).to be_nil }
end
+
+ context 'when cron is scheduled to a non existent day' do
+ let(:cron) { '0 12 31 2 *' }
+ let(:cron_timezone) { 'UTC' }
+
+ it { expect(subject).to be_nil }
+ end
end
describe '#cron_valid?' do
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
index 49a423191bb..dce56bbd2c4 100644
--- a/spec/lib/gitlab/favicon_spec.rb
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
expect(described_class.main).to match_asset_path '/assets/favicon.png'
end
- it 'has blue favicon for development' do
+ it 'has blue favicon for development', unless: Gitlab.ee? do
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
expect(described_class.main).to match_asset_path '/assets/favicon-blue.png'
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index cfc3e0ce926..bc4f867e891 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -91,7 +91,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
end
it 'has issue comments' do
- expect(saved_project_json['issues'].first['notes']).not_to be_empty
+ notes = saved_project_json['issues'].first['notes']
+
+ expect(notes).not_to be_empty
+ expect(notes.first['type']).to eq('DiscussionNote')
end
it 'has issue assignees' do
@@ -299,7 +302,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
create(:commit_status, project: project, pipeline: ci_build.pipeline)
create(:milestone, project: project)
- create(:note, noteable: issue, project: project)
+ create(:discussion_note, noteable: issue, project: project)
create(:note, noteable: merge_request, project: project)
create(:note, noteable: snippet, project: project)
create(:note_on_commit,
diff --git a/spec/lib/gitlab/lets_encrypt/challenge_spec.rb b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
new file mode 100644
index 00000000000..74622f356de
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Challenge do
+ delegated_methods = {
+ url: 'https://example.com/',
+ status: 'pending',
+ token: 'tokenvalue',
+ file_content: 'hereisfilecontent',
+ request_validation: true
+ }
+
+ let(:acme_challenge) do
+ acme_challenge = instance_double('Acme::Client::Resources::Challenge')
+ allow(acme_challenge).to receive_messages(delegated_methods)
+ acme_challenge
+ end
+
+ let(:challenge) { described_class.new(acme_challenge) }
+
+ delegated_methods.each do |method, value|
+ describe "##{method}" do
+ it 'delegates to Acme::Client::Resources::Challenge' do
+ expect(challenge.public_send(method)).to eq(value)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb
new file mode 100644
index 00000000000..16a16acfd25
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Client do
+ include LetsEncryptHelpers
+
+ let(:client) { described_class.new }
+
+ before do
+ stub_application_setting(
+ lets_encrypt_notification_email: 'myemail@test.example.com',
+ lets_encrypt_terms_of_service_accepted: true
+ )
+ end
+
+ let!(:stub_client) { stub_lets_encrypt_client }
+
+ shared_examples 'ensures account registration' do
+ it 'ensures account registration' do
+ subject
+
+ expect(stub_client).to have_received(:new_account).with(
+ contact: 'mailto:myemail@test.example.com',
+ terms_of_service_agreed: true
+ )
+ end
+
+ context 'when acme integration is disabled' do
+ before do
+ stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
+ end
+
+ it 'raises error' do
+ expect do
+ subject
+ end.to raise_error('Acme integration is disabled')
+ end
+ end
+ end
+
+ describe '#new_order' do
+ subject(:new_order) { client.new_order('example.com') }
+
+ before do
+ order_double = instance_double('Acme::Order')
+ allow(stub_client).to receive(:new_order).and_return(order_double)
+ end
+
+ include_examples 'ensures account registration'
+
+ it 'returns order' do
+ is_expected.to be_a(::Gitlab::LetsEncrypt::Order)
+ end
+ end
+
+ describe '#load_order' do
+ let(:url) { 'https://example.com/order' }
+ subject { client.load_order(url) }
+
+ before do
+ acme_order = instance_double('Acme::Client::Resources::Order')
+ allow(stub_client).to receive(:order).with(url: url).and_return(acme_order)
+ end
+
+ include_examples 'ensures account registration'
+
+ it 'loads order' do
+ is_expected.to be_a(::Gitlab::LetsEncrypt::Order)
+ end
+ end
+
+ describe '#load_challenge' do
+ let(:url) { 'https://example.com/challenge' }
+ subject { client.load_challenge(url) }
+
+ before do
+ acme_challenge = instance_double('Acme::Client::Resources::Challenge')
+ allow(stub_client).to receive(:challenge).with(url: url).and_return(acme_challenge)
+ end
+
+ include_examples 'ensures account registration'
+
+ it 'loads challenge' do
+ is_expected.to be_a(::Gitlab::LetsEncrypt::Challenge)
+ end
+ end
+
+ describe '#enabled?' do
+ subject { client.enabled? }
+
+ context 'when terms of service are accepted' do
+ it { is_expected.to eq(true) }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when terms of service are not accepted' do
+ before do
+ stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#terms_of_service_url' do
+ subject { client.terms_of_service_url }
+
+ it 'returns valid url' do
+ is_expected.to eq("https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/lets_encrypt/order_spec.rb b/spec/lib/gitlab/lets_encrypt/order_spec.rb
new file mode 100644
index 00000000000..ee7058baf8d
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/order_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Order do
+ delegated_methods = {
+ url: 'https://example.com/',
+ status: 'valid'
+ }
+
+ let(:acme_order) do
+ acme_order = instance_double('Acme::Client::Resources::Order')
+ allow(acme_order).to receive_messages(delegated_methods)
+ acme_order
+ end
+
+ let(:order) { described_class.new(acme_order) }
+
+ delegated_methods.each do |method, value|
+ describe "##{method}" do
+ it 'delegates to Acme::Client::Resources::Order' do
+ expect(order.public_send(method)).to eq(value)
+ end
+ end
+ end
+
+ describe '#new_challenge' do
+ before do
+ challenge = instance_double('Acme::Client::Resources::Challenges::HTTP01')
+ authorization = instance_double('Acme::Client::Resources::Authorization')
+ allow(authorization).to receive(:http).and_return(challenge)
+ allow(acme_order).to receive(:authorizations).and_return([authorization])
+ end
+
+ it 'returns challenge' do
+ expect(order.new_challenge).to be_a(::Gitlab::LetsEncrypt::Challenge)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index aaf8c9fa2a0..7972ff253fe 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -10,20 +10,17 @@ describe Gitlab::Metrics::Samplers::RubySampler do
describe '#sample' do
it 'samples various statistics' do
- expect(Gitlab::Metrics::System).to receive(:cpu_time)
- expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
expect(Gitlab::Metrics::System).to receive(:memory_usage)
- expect(Gitlab::Metrics::System).to receive(:process_start_time)
- expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors)
+ expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
expect(sampler).to receive(:sample_gc)
sampler.sample
end
- it 'adds a metric containing the process resident memory bytes' do
+ it 'adds a metric containing the memory usage' do
expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000)
- expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000)
+ expect(sampler.metrics[:memory_usage]).to receive(:set).with({}, 9000)
sampler.sample
end
@@ -37,27 +34,6 @@ describe Gitlab::Metrics::Samplers::RubySampler do
sampler.sample
end
- it 'adds a metric containing the process total cpu time' do
- expect(Gitlab::Metrics::System).to receive(:cpu_time).and_return(0.51)
- expect(sampler.metrics[:process_cpu_seconds_total]).to receive(:set).with({}, 0.51)
-
- sampler.sample
- end
-
- it 'adds a metric containing the process start time' do
- expect(Gitlab::Metrics::System).to receive(:process_start_time).and_return(12345)
- expect(sampler.metrics[:process_start_time_seconds]).to receive(:set).with({}, 12345)
-
- sampler.sample
- end
-
- it 'adds a metric containing the process max file descriptors' do
- expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors).and_return(1024)
- expect(sampler.metrics[:process_max_fds]).to receive(:set).with({}, 1024)
-
- sampler.sample
- end
-
it 'clears any GC profiles' do
expect(GC::Profiler).to receive(:clear)
diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
index 090e456644f..4b03f3c2532 100644
--- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
@@ -39,8 +39,8 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
it 'updates metrics type unix and with addr' do
labels = { socket_type: 'unix', socket_address: socket_address }
- expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
- expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
+ expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
+ expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
subject.sample
end
@@ -50,6 +50,7 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
context 'unicorn listens on tcp sockets' do
let(:tcp_socket_address) { '0.0.0.0:8080' }
let(:tcp_sockets) { [tcp_socket_address] }
+
before do
allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
end
@@ -70,29 +71,13 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
it 'updates metrics type unix and with addr' do
labels = { socket_type: 'tcp', socket_address: tcp_socket_address }
- expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
- expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
+ expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
+ expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
subject.sample
end
end
end
-
- context 'additional metrics' do
- let(:unicorn_workers) { 2 }
-
- before do
- allow(unicorn).to receive(:listener_names).and_return([""])
- allow(::Gitlab::Metrics::System).to receive(:cpu_time).and_return(3.14)
- allow(subject).to receive(:unicorn_workers_count).and_return(unicorn_workers)
- end
-
- it "sets additional metrics" do
- expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, unicorn_workers)
-
- subject.sample
- end
- end
end
describe '#start' do
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index b0603d96eb2..14afcdf5daa 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -13,18 +13,6 @@ describe Gitlab::Metrics::System do
expect(described_class.file_descriptor_count).to be > 0
end
end
-
- describe '.max_open_file_descriptors' do
- it 'returns the max allowed open file descriptors' do
- expect(described_class.max_open_file_descriptors).to be > 0
- end
- end
-
- describe '.process_start_time' do
- it 'returns the process start time' do
- expect(described_class.process_start_time).to be > 0
- end
- end
else
describe '.memory_usage' do
it 'returns 0.0' do
@@ -37,18 +25,6 @@ describe Gitlab::Metrics::System do
expect(described_class.file_descriptor_count).to eq(0)
end
end
-
- describe '.max_open_file_descriptors' do
- it 'returns 0' do
- expect(described_class.max_open_file_descriptors).to eq(0)
- end
- end
-
- describe 'process_start_time' do
- it 'returns 0' do
- expect(described_class.process_start_time).to eq(0)
- end
- end
end
describe '.cpu_time' do
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
index ae522a588ee..af8b059b984 100644
--- a/spec/lib/gitlab/sentry_spec.rb
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -2,12 +2,15 @@ require 'spec_helper'
describe Gitlab::Sentry do
describe '.context' do
- it 'adds the locale to the tags' do
+ it 'adds the expected tags' do
expect(described_class).to receive(:enabled?).and_return(true)
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
described_class.context(nil)
expect(Raven.tags_context[:locale].to_s).to eq(I18n.locale.to_s)
+ expect(Raven.tags_context[Labkit::Correlation::CorrelationId::LOG_KEY.to_sym].to_s)
+ .to eq('cid')
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index aa975c8bb0b..e44463dd767 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -63,12 +63,7 @@ describe Gitlab::UsageData do
end
it "gathers usage counts" do
- count_data = subject[:counts]
-
- expect(count_data[:boards]).to eq(1)
- expect(count_data[:projects]).to eq(3)
-
- expect(count_data.keys).to include(*%i(
+ expected_keys = %i(
assignee_lists
boards
ci_builds
@@ -112,6 +107,7 @@ describe Gitlab::UsageData do
milestone_lists
milestones
notes
+ pool_repositories
projects
projects_imported_from_github
projects_jira_active
@@ -132,7 +128,14 @@ describe Gitlab::UsageData do
uploads
web_hooks
user_preferences
- ))
+ )
+
+ count_data = subject[:counts]
+
+ expect(count_data[:boards]).to eq(1)
+ expect(count_data[:projects]).to eq(3)
+ expect(count_data.keys).to include(*expected_keys)
+ expect(expected_keys - count_data.keys).to be_empty
end
it 'does not gather user preferences usage data when the feature is disabled' do
@@ -211,7 +214,7 @@ describe Gitlab::UsageData do
it "gathers license data" do
expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid)
expect(subject[:version]).to eq(Gitlab::VERSION)
- expect(subject[:installation_type]).to eq(Gitlab::INSTALLATION_TYPE)
+ expect(subject[:installation_type]).to eq('gitlab-development-kit')
expect(subject[:active_user_count]).to eq(User.active.count)
expect(subject[:recorded_at]).to be_a(Time)
end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
index b523f393ece..2762eaeccd3 100644
--- a/spec/models/active_session_spec.rb
+++ b/spec/models/active_session_spec.rb
@@ -88,6 +88,52 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end
end
+ describe '.list_sessions' do
+ it 'uses the ActiveSession lookup to return original sessions' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ _csrf_token: 'abcd' }))
+
+ redis.sadd(
+ "session:lookup:user:gitlab:#{user.id}",
+ %w[
+ 6919a6f1bb119dd7396fadc38fd18d0d
+ 59822c7d9fcdfa03725eff41782ad97d
+ ]
+ )
+ end
+
+ expect(ActiveSession.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }]
+ end
+ end
+
+ describe '.session_ids_for_user' do
+ it 'uses the user lookup table to return session ids' do
+ session_ids = ['59822c7d9fcdfa03725eff41782ad97d']
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", session_ids)
+ end
+
+ expect(ActiveSession.session_ids_for_user(user)).to eq(session_ids)
+ end
+ end
+
+ describe '.sessions_from_ids' do
+ it 'uses the ActiveSession lookup to return original sessions' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ _csrf_token: 'abcd' }))
+ end
+
+ expect(ActiveSession.sessions_from_ids(['6919a6f1bb119dd7396fadc38fd18d0d'])).to eq [{ _csrf_token: 'abcd' }]
+ end
+
+ it 'avoids a redis lookup for an empty array' do
+ expect(Gitlab::Redis::SharedState).not_to receive(:with)
+
+ expect(ActiveSession.sessions_from_ids([])).to eq([])
+ end
+ end
+
describe '.set' do
it 'sets a new redis entry for the user session and a lookup entry' do
ActiveSession.set(user, request)
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index cc90a998d3f..74573d0941c 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -52,4 +52,10 @@ describe ApplicationRecord do
expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid)
end
end
+
+ describe '.underscore' do
+ it 'returns the underscored value of the class as a string' do
+ expect(MergeRequest.underscore).to eq('merge_request')
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 9b489baf163..5f2e8aa0baa 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2925,26 +2925,18 @@ describe Ci::Build do
subject { build.any_unmet_prerequisites? }
+ before do
+ allow(build).to receive(:prerequisites).and_return(prerequisites)
+ end
+
context 'build has prerequisites' do
- before do
- allow(build).to receive(:prerequisites).and_return([double])
- end
+ let(:prerequisites) { [double] }
it { is_expected.to be_truthy }
-
- context 'and the ci_preparing_state feature is disabled' do
- before do
- stub_feature_flags(ci_preparing_state: false)
- end
-
- it { is_expected.to be_falsey }
- end
end
context 'build does not have prerequisites' do
- before do
- allow(build).to receive(:prerequisites).and_return([])
- end
+ let(:prerequisites) { [] }
it { is_expected.to be_falsey }
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index e91b5c4c86f..62663c247d1 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -88,7 +88,7 @@ describe Event do
let(:event) { create_push_event(project, user) }
it do
- expect(event.push?).to be_truthy
+ expect(event.push_action?).to be_truthy
expect(event.visible_to_user?(user)).to be_truthy
expect(event.visible_to_user?(nil)).to be_falsey
expect(event.tag?).to be_falsey
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2a17bd6002e..425096d7e80 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -214,6 +214,13 @@ describe Project do
expect(project2).not_to be_valid
end
+ it 'validates the visibility' do
+ expect_any_instance_of(described_class).to receive(:visibility_level_allowed_as_fork).and_call_original
+ expect_any_instance_of(described_class).to receive(:visibility_level_allowed_by_group).and_call_original
+
+ create(:project)
+ end
+
describe 'wiki path conflict' do
context "when the new path has been used by the wiki of other Project" do
it 'has an error on the name attribute' do
diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb
index f86500f91cd..5509ed87308 100644
--- a/spec/models/push_event_spec.rb
+++ b/spec/models/push_event_spec.rb
@@ -123,9 +123,9 @@ describe PushEvent do
end
end
- describe '#push?' do
+ describe '#push_action?' do
it 'returns true' do
- expect(event).to be_push
+ expect(event).to be_push_action
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 9ff0f355fd4..c5ab7e57272 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2286,12 +2286,45 @@ describe Repository do
end
describe '#diverging_commit_counts' do
+ let(:diverged_branch) { repository.find_branch('fix') }
+ let(:root_ref_sha) { repository.raw_repository.commit(repository.root_ref).id }
+ let(:diverged_branch_sha) { diverged_branch.dereferenced_target.sha }
+
it 'returns the commit counts behind and ahead of default branch' do
- result = repository.diverging_commit_counts(
- repository.find_branch('fix'))
+ result = repository.diverging_commit_counts(diverged_branch)
expect(result).to eq(behind: 29, ahead: 2)
end
+
+ context 'when gitaly_count_diverging_commits_no_max is enabled' do
+ before do
+ stub_feature_flags(gitaly_count_diverging_commits_no_max: true)
+ end
+
+ it 'calls diverging_commit_count without max count' do
+ expect(repository.raw_repository)
+ .to receive(:diverging_commit_count)
+ .with(root_ref_sha, diverged_branch_sha)
+ .and_return([29, 2])
+
+ repository.diverging_commit_counts(diverged_branch)
+ end
+ end
+
+ context 'when gitaly_count_diverging_commits_no_max is disabled' do
+ before do
+ stub_feature_flags(gitaly_count_diverging_commits_no_max: false)
+ end
+
+ it 'calls diverging_commit_count with max count' do
+ expect(repository.raw_repository)
+ .to receive(:diverging_commit_count)
+ .with(root_ref_sha, diverged_branch_sha, max_count: Repository::MAX_DIVERGING_COUNT)
+ .and_return([29, 2])
+
+ repository.diverging_commit_counts(diverged_branch)
+ end
+ end
end
describe '#refresh_method_caches' do
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 16036297ec7..ca1ffe3c524 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -13,7 +13,7 @@ describe API::Discussions do
let!(:issue) { create(:issue, project: project, author: user) }
let!(:issue_note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: user) }
- it_behaves_like 'discussions API', 'projects', 'issues', 'iid', can_reply_to_invididual_notes: true do
+ it_behaves_like 'discussions API', 'projects', 'issues', 'iid', can_reply_to_individual_notes: true do
let(:parent) { project }
let(:noteable) { issue }
let(:note) { issue_note }
@@ -37,7 +37,7 @@ describe API::Discussions do
let!(:diff_note) { create(:diff_note_on_merge_request, noteable: noteable, project: project, author: user) }
let(:parent) { project }
- it_behaves_like 'discussions API', 'projects', 'merge_requests', 'iid', can_reply_to_invididual_notes: true
+ it_behaves_like 'discussions API', 'projects', 'merge_requests', 'iid', can_reply_to_individual_notes: true
it_behaves_like 'diff discussions API', 'projects', 'merge_requests', 'iid'
it_behaves_like 'resolvable discussions API', 'projects', 'merge_requests', 'iid'
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 25a312cb734..ed907841bd8 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -247,9 +247,8 @@ describe API::Helpers do
exception = RuntimeError.new('test error')
allow(exception).to receive(:backtrace).and_return(caller)
- expect(Raven).to receive(:capture_exception).with(exception, tags: {
- correlation_id: 'new-correlation-id'
- }, extra: {})
+ expect(Raven).to receive(:capture_exception).with(exception, tags:
+ a_hash_including(correlation_id: 'new-correlation-id'), extra: {})
Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do
handle_api_exception(exception)
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
new file mode 100644
index 00000000000..8b02cf56e9f
--- /dev/null
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -0,0 +1,652 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Issues do
+ set(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
+
+ let(:no_milestone_title) { 'None' }
+ let(:any_milestone_title) { 'Any' }
+
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ end
+
+ describe 'GET /groups/:id/issues' do
+ let!(:group) { create(:group) }
+ let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
+ let!(:group_closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: group_project,
+ state: :closed,
+ milestone: group_milestone,
+ updated_at: 3.hours.ago,
+ created_at: 1.day.ago
+ end
+ let!(:group_confidential_issue) do
+ create :issue,
+ :confidential,
+ project: group_project,
+ author: author,
+ assignees: [assignee],
+ updated_at: 2.hours.ago,
+ created_at: 2.days.ago
+ end
+ let!(:group_issue) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: group_project,
+ milestone: group_milestone,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description,
+ created_at: 5.days.ago
+ end
+ let!(:group_label) do
+ create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
+ end
+ let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
+ let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
+ let!(:group_empty_milestone) do
+ create(:milestone, title: '4.0.0', project: group_project)
+ end
+ let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
+
+ let(:base_url) { "/groups/#{group.id}/issues" }
+
+ shared_examples 'group issues statistics' do
+ it 'returns issues statistics' do
+ get api("/groups/#{group.id}/issues_statistics", user), params: params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['statistics']).not_to be_nil
+ expect(json_response['statistics']['counts']['all']).to eq counts[:all]
+ expect(json_response['statistics']['counts']['closed']).to eq counts[:closed]
+ expect(json_response['statistics']['counts']['opened']).to eq counts[:opened]
+ end
+ end
+
+ context 'when group has subgroups', :nested_groups do
+ let(:subgroup_1) { create(:group, parent: group) }
+ let(:subgroup_2) { create(:group, parent: subgroup_1) }
+
+ let(:subgroup_1_project) { create(:project, :public, namespace: subgroup_1) }
+ let(:subgroup_2_project) { create(:project, namespace: subgroup_2) }
+
+ let!(:issue_1) { create(:issue, project: subgroup_1_project) }
+ let!(:issue_2) { create(:issue, project: subgroup_2_project) }
+
+ context 'when user is unauthenticated' do
+ it 'also returns subgroups public projects issues' do
+ get api(base_url)
+
+ expect_paginated_array_response([issue_1.id, group_closed_issue.id, group_issue.id])
+ end
+
+ it 'also returns subgroups public projects issues filtered by milestone' do
+ get api(base_url), params: { milestone: group_milestone.title }
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: group_milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: group_milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+ end
+ end
+
+ context 'when user is a group member' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'also returns subgroups projects issues' do
+ get api(base_url, user)
+
+ expect_paginated_array_response([issue_2.id, issue_1.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'also returns subgroups public projects issues filtered by milestone' do
+ get api(base_url, user), params: { milestone: group_milestone.title }
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 5, closed: 1, opened: 4 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 5, closed: 1, opened: 4 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 5, closed: 1, opened: 4 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 5, closed: 1, opened: 4 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: group_milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: group_milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+ end
+ end
+ end
+
+ context 'when user is unauthenticated' do
+ it 'lists all issues in public projects' do
+ get api(base_url)
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
+ it 'also returns subgroups public projects issues filtered by milestone' do
+ get api(base_url), params: { milestone: group_milestone.title }
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: group_milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: group_milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+ end
+ end
+
+ context 'when user is a group member' do
+ before do
+ group_project.add_reporter(user)
+ end
+
+ it 'returns all group issues (including opened and closed)' do
+ get api(base_url, admin)
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns group issues without confidential issues for non project members' do
+ get api(base_url, non_member), params: { state: :opened }
+
+ expect_paginated_array_response(group_issue.id)
+ end
+
+ it 'returns group confidential issues for author' do
+ get api(base_url, author), params: { state: :opened }
+
+ expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns group confidential issues for assignee' do
+ get api(base_url, assignee), params: { state: :opened }
+
+ expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns group issues with confidential issues for project members' do
+ get api(base_url, user), params: { state: :opened }
+
+ expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns group confidential issues for admin' do
+ get api(base_url, admin), params: { state: :opened }
+
+ expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns only confidential issues' do
+ get api(base_url, user), params: { confidential: true }
+
+ expect_paginated_array_response(group_confidential_issue.id)
+ end
+
+ it 'returns only public issues' do
+ get api(base_url, user), params: { confidential: false }
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
+ it 'returns an array of labeled group issues' do
+ get api(base_url, user), params: { labels: group_label.title }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues with labels param as array' do
+ get api(base_url, user), params: { labels: [group_label.title] }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues where all labels match' do
+ get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of labeled group issues where all labels match with labels param as array' do
+ get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns issues matching given search string for title' do
+ get api(base_url, user), params: { search: group_issue.title }
+
+ expect_paginated_array_response(group_issue.id)
+ end
+
+ it 'returns issues matching given search string for description' do
+ get api(base_url, user), params: { search: group_issue.description }
+
+ expect_paginated_array_response(group_issue.id)
+ end
+
+ context 'with labeled issues' do
+ let(:label_b) { create(:label, title: 'foo', project: group_project) }
+ let(:label_c) { create(:label, title: 'bar', project: group_project) }
+
+ before do
+ create(:label_link, label: label_b, target: group_issue)
+ create(:label_link, label: label_c, target: group_issue)
+
+ get api(base_url, user), params: params
+ end
+
+ let(:issue) { group_issue }
+ let(:label) { group_label }
+
+ it_behaves_like 'labeled issues with labels and label_name params'
+ end
+
+ it 'returns an array of issues found by iids' do
+ get api(base_url, user), params: { iids: [group_issue.iid] }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns an empty array if iid does not exist' do
+ get api(base_url, user), params: { iids: [0] }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if no group issue matches labels' do
+ get api(base_url, user), params: { labels: 'foo,bar' }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of group issues with any label' do
+ get api(base_url, user), params: { labels: IssuesFinder::FILTER_ANY }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns an array of group issues with any label with labels param as array' do
+ get api(base_url, user), params: { labels: [IssuesFinder::FILTER_ANY] }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns an array of group issues with no label' do
+ get api(base_url, user), params: { labels: IssuesFinder::FILTER_NONE }
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
+ end
+
+ it 'returns an array of group issues with no label with labels param as array' do
+ get api(base_url, user), params: { labels: [IssuesFinder::FILTER_NONE] }
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get api(base_url, user), params: { milestone: group_empty_milestone.title }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api(base_url, user), params: { milestone: 'foo' }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get api(base_url, user), params: { state: :opened, milestone: group_milestone.title }
+
+ expect_paginated_array_response(group_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api(base_url, user), params: { milestone: group_milestone.title, state: :closed }
+
+ expect_paginated_array_response(group_closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get api(base_url, user), params: { milestone: no_milestone_title }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect_paginated_array_response(group_confidential_issue.id)
+ end
+
+ context 'without sort params' do
+ it 'sorts by created_at descending by default' do
+ get api(base_url, user)
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ context 'with 2 issues with same created_at' do
+ let!(:group_issue2) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: group_project,
+ milestone: group_milestone,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description,
+ created_at: group_issue.created_at
+ end
+
+ it 'page breaks first page correctly' do
+ get api("#{base_url}?per_page=3", user)
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id])
+ end
+
+ it 'page breaks second page correctly' do
+ get api("#{base_url}?per_page=3&page=2", user)
+
+ expect_paginated_array_response([group_issue.id])
+ end
+ end
+ end
+
+ it 'sorts ascending when requested' do
+ get api("#{base_url}?sort=asc", user)
+
+ expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get api("#{base_url}?order_by=updated_at", user)
+
+ group_issue.touch(:updated_at)
+
+ expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api(base_url, user), params: { order_by: :updated_at, sort: :asc }
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: group_milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: group_milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'sort does not affect statistics ' do
+ let(:params) { { state: :opened, order_by: 'updated_at' } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+ end
+
+ context 'filtering by assignee_username' do
+ let(:another_assignee) { create(:assignee) }
+ let!(:issue1) { create(:issue, author: user2, project: group_project, created_at: 3.days.ago) }
+ let!(:issue2) { create(:issue, author: user2, project: group_project, created_at: 2.days.ago) }
+ let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: group_project, created_at: 1.day.ago) }
+
+ it 'returns issues with by assignee_username' do
+ get api(base_url, user), params: { assignee_username: [assignee.username], scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([issue3.id, group_confidential_issue.id])
+ end
+
+ it 'returns issues by assignee_username as string' do
+ get api(base_url, user), params: { assignee_username: assignee.username, scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([issue3.id, group_confidential_issue.id])
+ end
+
+ it 'returns error when multiple assignees are passed' do
+ get api(base_url, user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("allows one value, but found 2")
+ end
+
+ it 'returns error when assignee_username and assignee_id are passed together' do
+ get api(base_url, user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("mutually exclusive")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
new file mode 100644
index 00000000000..a07d7673345
--- /dev/null
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -0,0 +1,805 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Issues do
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 3.hours.ago,
+ closed_at: 1.hour.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignees: [assignee],
+ created_at: generate(:past_time),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+ set(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ set(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { 'None' }
+ let(:any_milestone_title) { 'Any' }
+
+ before(:all) do
+ project.add_reporter(user)
+ project.add_guest(guest)
+ end
+
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ end
+
+ shared_examples 'project issues statistics' do
+ it 'returns project issues statistics' do
+ get api("/issues_statistics", user), params: params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['statistics']).not_to be_nil
+ expect(json_response['statistics']['counts']['all']).to eq counts[:all]
+ expect(json_response['statistics']['counts']['closed']).to eq counts[:closed]
+ expect(json_response['statistics']['counts']['opened']).to eq counts[:opened]
+ end
+ end
+
+ describe "GET /projects/:id/issues" do
+ let(:base_url) { "/projects/#{project.id}" }
+
+ context 'when unauthenticated' do
+ it 'returns public project issues' do
+ get api("/projects/#{project.id}/issues")
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'sort does not affect statistics ' do
+ let(:params) { { state: :opened, order_by: 'updated_at' } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ get api("/projects/#{project.id}/issues", user)
+
+ create_list(:issue, 3, project: project)
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{project.id}/issues", user)
+ end.count
+
+ expect do
+ get api("/projects/#{project.id}/issues", user)
+ end.not_to exceed_all_query_limit(control_count)
+ end
+
+ it 'returns 404 when project does not exist' do
+ get api('/projects/1000/issues', non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 on private projects for other users' do
+ private_project = create(:project, :private)
+ create(:issue, project: private_project)
+
+ get api("/projects/#{private_project.id}/issues", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns no issues when user has access to project but not issues' do
+ restricted_project = create(:project, :public, :issues_private)
+ create(:issue, project: restricted_project)
+
+ get api("/projects/#{restricted_project.id}/issues", non_member)
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns project issues without confidential issues for non project members' do
+ get api("#{base_url}/issues", non_member)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns project issues without confidential issues for project members with guest role' do
+ get api("#{base_url}/issues", guest)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns project confidential issues for author' do
+ get api("#{base_url}/issues", author)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns only confidential issues' do
+ get api("#{base_url}/issues", author), params: { confidential: true }
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns only public issues' do
+ get api("#{base_url}/issues", author), params: { confidential: false }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns project confidential issues for assignee' do
+ get api("#{base_url}/issues", assignee)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns project issues with confidential issues for project members' do
+ get api("#{base_url}/issues", user)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns project confidential issues for admin' do
+ get api("#{base_url}/issues", admin)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns an array of labeled project issues' do
+ get api("#{base_url}/issues", user), params: { labels: label.title }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of labeled project issues with labels param as array' do
+ get api("#{base_url}/issues", user), params: { labels: [label.title] }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ context 'with labeled issues' do
+ let(:label_b) { create(:label, title: 'foo', project: project) }
+ let(:label_c) { create(:label, title: 'bar', project: project) }
+
+ before do
+ create(:label_link, label: label_b, target: issue)
+ create(:label_link, label: label_c, target: issue)
+
+ get api('/issues', user), params: params
+ end
+
+ it_behaves_like 'labeled issues with labels and label_name params'
+ end
+
+ it 'returns issues matching given search string for title' do
+ get api("#{base_url}/issues?search=#{issue.title}", user)
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns issues matching given search string for description' do
+ get api("#{base_url}/issues?search=#{issue.description}", user)
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of issues found by iids' do
+ get api("#{base_url}/issues", user), params: { iids: [issue.iid] }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an empty array if iid does not exist' do
+ get api("#{base_url}/issues", user), params: { iids: [0] }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if not all labels matches' do
+ get api("#{base_url}/issues?labels=#{label.title},foo", user)
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of project issues with any label' do
+ get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_ANY }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of project issues with any label with labels param as array' do
+ get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_ANY] }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of project issues with no label' do
+ get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_NONE }
+
+ expect_paginated_array_response([confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns an array of project issues with no label with labels param as array' do
+ get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_NONE] }
+
+ expect_paginated_array_response([confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns an empty array if no project issue matches labels' do
+ get api("#{base_url}/issues", user), params: { labels: 'foo,bar' }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get api("#{base_url}/issues", user), params: { milestone: empty_milestone.title }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api("#{base_url}/issues", user), params: { milestone: :foo }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get api("#{base_url}/issues", user), params: { milestone: milestone.title }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api("#{base_url}/issues", user), params: { milestone: milestone.title, state: :closed }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get api("#{base_url}/issues", user), params: { milestone: no_milestone_title }
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns an array of issues with any milestone' do
+ get api("#{base_url}/issues", user), params: { milestone: any_milestone_title }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ context 'without sort params' do
+ it 'sorts by created_at descending by default' do
+ get api("#{base_url}/issues", user)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ context 'with 2 issues with same created_at' do
+ let!(:closed_issue2) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: closed_issue.created_at,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+
+ it 'page breaks first page correctly' do
+ get api("#{base_url}/issues?per_page=3", user)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id])
+ end
+
+ it 'page breaks second page correctly' do
+ get api("#{base_url}/issues?per_page=3&page=2", user)
+
+ expect_paginated_array_response([closed_issue.id])
+ end
+ end
+ end
+
+ it 'sorts ascending when requested' do
+ get api("#{base_url}/issues", user), params: { sort: :asc }
+
+ expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get api("#{base_url}/issues", user), params: { order_by: :updated_at }
+
+ issue.touch(:updated_at)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api("#{base_url}/issues", user), params: { order_by: :updated_at, sort: :asc }
+
+ expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'sort does not affect statistics ' do
+ let(:params) { { state: :opened, order_by: 'updated_at' } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+ end
+
+ context 'filtering by assignee_username' do
+ let(:another_assignee) { create(:assignee) }
+ let!(:issue1) { create(:issue, author: user2, project: project, created_at: 3.days.ago) }
+ let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) }
+ let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) }
+
+ it 'returns issues by assignee_username' do
+ get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([confidential_issue.id, issue3.id])
+ end
+
+ it 'returns issues by assignee_username as string' do
+ get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([confidential_issue.id, issue3.id])
+ end
+
+ it 'returns error when multiple assignees are passed' do
+ get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("allows one value, but found 2")
+ end
+
+ it 'returns error when assignee_username and assignee_id are passed together' do
+ get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("mutually exclusive")
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/issues/:issue_iid' do
+ context 'when unauthenticated' do
+ it 'returns public issues' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}")
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ it 'exposes known attributes' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(issue.id)
+ expect(json_response['iid']).to eq(issue.iid)
+ expect(json_response['project_id']).to eq(issue.project.id)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['description']).to eq(issue.description)
+ expect(json_response['state']).to eq(issue.state)
+ expect(json_response['closed_at']).to be_falsy
+ expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
+ expect(json_response['labels']).to eq(issue.label_names)
+ expect(json_response['milestone']).to be_a Hash
+ expect(json_response['assignees']).to be_a Array
+ expect(json_response['assignee']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'exposes the closed_at attribute' do
+ get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['closed_at']).to be_present
+ end
+
+ context 'links exposure' do
+ it 'exposes related resources full URIs' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}", user)
+
+ links = json_response['_links']
+
+ expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}")
+ expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes")
+ expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji")
+ expect(links['project']).to end_with("/api/v4/projects/#{project.id}")
+ end
+ end
+
+ it 'returns a project issue by internal id' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['iid']).to eq(issue.iid)
+ end
+
+ it 'returns 404 if issue id not found' do
+ get api("/projects/#{project.id}/issues/54321", user)
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the issue ID is used' do
+ get api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ context 'confidential issues' do
+ it 'returns 404 for non project members' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 for project members with guest role' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns confidential issue for project members' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it 'returns confidential issue for author' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it 'returns confidential issue for assignee' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it 'returns confidential issue for admin' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+ end
+ end
+
+ describe 'GET :id/issues/:issue_iid/closed_by' do
+ let(:merge_request) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "closes #{issue.to_reference}")
+ end
+
+ before do
+ create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
+ end
+
+ context 'when unauthenticated' do
+ it 'return public project issues' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
+
+ expect_paginated_array_response(merge_request.id)
+ end
+ end
+
+ it 'returns merge requests that will close issue on merge' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
+
+ expect_paginated_array_response(merge_request.id)
+ end
+
+ context 'when no merge requests will close issue' do
+ it 'returns empty array' do
+ get api("/projects/#{project.id}/issues/#{closed_issue.iid}/closed_by", user)
+
+ expect_paginated_array_response([])
+ end
+ end
+
+ it "returns 404 when issue doesn't exists" do
+ get api("/projects/#{project.id}/issues/0/closed_by", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe 'GET :id/issues/:issue_iid/related_merge_requests' do
+ def get_related_merge_requests(project_id, issue_iid, user = nil)
+ get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
+ end
+
+ def create_referencing_mr(user, project, issue)
+ attributes = {
+ author: user,
+ source_project: project,
+ target_project: project,
+ source_branch: 'master',
+ target_branch: 'test',
+ description: "See #{issue.to_reference}"
+ }
+ create(:merge_request, attributes).tap do |merge_request|
+ create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
+ end
+ end
+
+ let!(:related_mr) { create_referencing_mr(user, project, issue) }
+
+ context 'when unauthenticated' do
+ it 'return list of referenced merge requests from issue' do
+ get_related_merge_requests(project.id, issue.iid)
+
+ expect_paginated_array_response(related_mr.id)
+ end
+
+ it 'renders 404 if project is not visible' do
+ private_project = create(:project, :private)
+ private_issue = create(:issue, project: private_project)
+ create_referencing_mr(user, private_project, private_issue)
+
+ get_related_merge_requests(private_project.id, private_issue.iid)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ it 'returns merge requests that mentioned a issue' do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: 'Some description')
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response(related_mr.id)
+ end
+
+ it 'returns merge requests cross-project wide' do
+ project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ merge_request = create_referencing_mr(user, project2, issue)
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response([related_mr.id, merge_request.id])
+ end
+
+ it 'does not generate references to projects with no access' do
+ private_project = create(:project, :private)
+ create_referencing_mr(private_project.creator, private_project, issue)
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response(related_mr.id)
+ end
+
+ context 'no merge request mentioned a issue' do
+ it 'returns empty array' do
+ get_related_merge_requests(project.id, closed_issue.iid, user)
+
+ expect_paginated_array_response([])
+ end
+ end
+
+ it "returns 404 when issue doesn't exists" do
+ get_related_merge_requests(project.id, 0, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/issues/:issue_iid/user_agent_detail' do
+ let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
+
+ context 'when unauthenticated' do
+ it 'returns unauthorized' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ it 'exposes known attributes' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
+ expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
+ expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
+ end
+
+ it 'returns unauthorized for non-admin users' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ describe 'GET projects/:id/issues/:issue_iid/participants' do
+ it_behaves_like 'issuable participants endpoint' do
+ let(:entity) { issue }
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
new file mode 100644
index 00000000000..9b9cc778fb3
--- /dev/null
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -0,0 +1,796 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Issues do
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 3.hours.ago,
+ closed_at: 1.hour.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignees: [assignee],
+ created_at: generate(:past_time),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+ set(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ set(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { 'None' }
+ let(:any_milestone_title) { 'Any' }
+
+ before(:all) do
+ project.add_reporter(user)
+ project.add_guest(guest)
+ end
+
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ end
+
+ shared_examples 'issues statistics' do
+ it 'returns issues statistics' do
+ get api("/issues_statistics", user), params: params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['statistics']).not_to be_nil
+ expect(json_response['statistics']['counts']['all']).to eq counts[:all]
+ expect(json_response['statistics']['counts']['closed']).to eq counts[:closed]
+ expect(json_response['statistics']['counts']['opened']).to eq counts[:opened]
+ end
+ end
+
+ describe 'GET /issues' do
+ context 'when unauthenticated' do
+ it 'returns an array of all issues' do
+ get api('/issues'), params: { scope: 'all' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns authentication error without any scope' do
+ get api('/issues')
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns authentication error when scope is assigned-to-me' do
+ get api('/issues'), params: { scope: 'assigned-to-me' }
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns authentication error when scope is created-by-me' do
+ get api('/issues'), params: { scope: 'created-by-me' }
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api('/issues'), params: { milestone: 'foo', scope: 'all' }
+
+ expect(response).to have_http_status(200)
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api('/issues'), params: { milestone: milestone.title, scope: 'all' }
+
+ expect(response).to have_http_status(200)
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ context 'issues_statistics' do
+ it 'returns authentication error without any scope' do
+ get api('/issues_statistics')
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns authentication error when scope is assigned_to_me' do
+ get api('/issues_statistics'), params: { scope: 'assigned_to_me' }
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns authentication error when scope is created_by_me' do
+ get api('/issues_statistics'), params: { scope: 'created_by_me' }
+
+ expect(response).to have_http_status(401)
+ end
+
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'sort does not affect statistics ' do
+ let(:params) { { state: :opened, order_by: 'updated_at' } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns an array of issues' do
+ get api('/issues', user)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.last).to have_key('web_url')
+ end
+
+ it 'returns an array of closed issues' do
+ get api('/issues', user), params: { state: :closed }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of opened issues' do
+ get api('/issues', user), params: { state: :opened }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of all issues' do
+ get api('/issues', user), params: { state: :all }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns issues assigned to me' do
+ issue2 = create(:issue, assignees: [user2], project: project)
+
+ get api('/issues', user2), params: { scope: 'assigned_to_me' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues assigned to me (kebab-case)' do
+ issue2 = create(:issue, assignees: [user2], project: project)
+
+ get api('/issues', user2), params: { scope: 'assigned-to-me' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues authored by the given author id' do
+ issue2 = create(:issue, author: user2, project: project)
+
+ get api('/issues', user), params: { author_id: user2.id, scope: 'all' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues assigned to the given assignee id' do
+ issue2 = create(:issue, assignees: [user2], project: project)
+
+ get api('/issues', user), params: { assignee_id: user2.id, scope: 'all' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues authored by the given author id and assigned to the given assignee id' do
+ issue2 = create(:issue, author: user2, assignees: [user2], project: project)
+
+ get api('/issues', user), params: { author_id: user2.id, assignee_id: user2.id, scope: 'all' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues with no assignee' do
+ issue2 = create(:issue, author: user2, project: project)
+
+ get api('/issues', user), params: { assignee_id: 0, scope: 'all' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues with no assignee' do
+ issue2 = create(:issue, author: user2, project: project)
+
+ get api('/issues', user), params: { assignee_id: 'None', scope: 'all' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues with any assignee' do
+ # This issue without assignee should not be returned
+ create(:issue, author: user2, project: project)
+
+ get api('/issues', user), params: { assignee_id: 'Any', scope: 'all' }
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns only confidential issues' do
+ get api('/issues', user), params: { confidential: true, scope: 'all' }
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns only public issues' do
+ get api('/issues', user), params: { confidential: false }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns issues reacted by the authenticated user' do
+ issue2 = create(:issue, project: project, author: user, assignees: [user])
+ create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+ create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup')
+
+ get api('/issues', user2), params: { my_reaction_emoji: 'Any', scope: 'all' }
+
+ expect_paginated_array_response([issue2.id, issue.id])
+ end
+
+ it 'returns issues not reacted by the authenticated user' do
+ issue2 = create(:issue, project: project, author: user, assignees: [user])
+ create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+
+ get api('/issues', user2), params: { my_reaction_emoji: 'None', scope: 'all' }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns issues matching given search string for title' do
+ get api('/issues', user), params: { search: issue.title }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns issues matching given search string for title and scoped in title' do
+ get api('/issues', user), params: { search: issue.title, in: 'title' }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an empty array if no issue matches given search string for title and scoped in description' do
+ get api('/issues', user), params: { search: issue.title, in: 'description' }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns issues matching given search string for description' do
+ get api('/issues', user), params: { search: issue.description }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ context 'filtering before a specific date' do
+ let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) }
+
+ it 'returns issues created before a specific date' do
+ get api('/issues?created_before=2000-01-02T00:00:00.060Z', user)
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues updated before a specific date' do
+ get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user)
+
+ expect_paginated_array_response(issue2.id)
+ end
+ end
+
+ context 'filtering after a specific date' do
+ let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) }
+
+ it 'returns issues created after a specific date' do
+ get api("/issues?created_after=#{issue2.created_at}", user)
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues updated after a specific date' do
+ get api("/issues?updated_after=#{issue2.updated_at}", user)
+
+ expect_paginated_array_response(issue2.id)
+ end
+ end
+
+ context 'filter by labels or label_name param' do
+ context 'N+1' do
+ let(:label_b) { create(:label, title: 'foo', project: project) }
+ let(:label_c) { create(:label, title: 'bar', project: project) }
+
+ before do
+ create(:label_link, label: label_b, target: issue)
+ create(:label_link, label: label_c, target: issue)
+ end
+ it 'tests N+1' do
+ control = ActiveRecord::QueryRecorder.new do
+ get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
+ end
+
+ label_d = create(:label, title: 'dar', project: project)
+ label_e = create(:label, title: 'ear', project: project)
+ create(:label_link, label: label_d, target: issue)
+ create(:label_link, label: label_e, target: issue)
+
+ expect do
+ get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
+ end.not_to exceed_query_limit(control)
+ expect(issue.labels.count).to eq(5)
+ end
+ end
+
+ it 'returns an array of labeled issues' do
+ get api('/issues', user), params: { labels: label.title }
+
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled issues with labels param as array' do
+ get api('/issues', user), params: { labels: [label.title] }
+
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ context 'with labeled issues' do
+ let(:label_b) { create(:label, title: 'foo', project: project) }
+ let(:label_c) { create(:label, title: 'bar', project: project) }
+
+ before do
+ create(:label_link, label: label_b, target: issue)
+ create(:label_link, label: label_c, target: issue)
+
+ get api('/issues', user), params: params
+ end
+
+ it_behaves_like 'labeled issues with labels and label_name params'
+ end
+
+ it 'returns an empty array if no issue matches labels' do
+ get api('/issues', user), params: { labels: 'foo,bar' }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if no issue matches labels with labels param as array' do
+ get api('/issues', user), params: { labels: %w(foo bar) }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of labeled issues matching given state' do
+ get api('/issues', user), params: { labels: label.title, state: :opened }
+
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels']).to eq([label.title])
+ expect(json_response.first['state']).to eq('opened')
+ end
+
+ it 'returns an array of labeled issues matching given state with labels param as array' do
+ get api('/issues', user), params: { labels: [label.title], state: :opened }
+
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels']).to eq([label.title])
+ expect(json_response.first['state']).to eq('opened')
+ end
+
+ it 'returns an empty array if no issue matches labels and state filters' do
+ get api('/issues', user), params: { labels: label.title, state: :closed }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of issues with any label' do
+ get api('/issues', user), params: { labels: IssuesFinder::FILTER_ANY }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of issues with any label with labels param as array' do
+ get api('/issues', user), params: { labels: [IssuesFinder::FILTER_ANY] }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of issues with no label' do
+ get api('/issues', user), params: { labels: IssuesFinder::FILTER_NONE }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no label with labels param as array' do
+ get api('/issues', user), params: { labels: [IssuesFinder::FILTER_NONE] }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no label when using the legacy No+Label filter' do
+ get api('/issues', user), params: { labels: 'No Label' }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no label when using the legacy No+Label filter with labels param as array' do
+ get api('/issues', user), params: { labels: ['No Label'] }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get api("/issues?milestone=#{empty_milestone.title}", user)
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api('/issues?milestone=foo', user)
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get api("/issues?milestone=#{milestone.title}", user)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns an array of issues in given milestone_title param' do
+ get api("/issues?milestone_title=#{milestone.title}", user)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api("/issues?milestone=#{milestone.title}&state=closed", user)
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get api("/issues?milestone=#{no_milestone_title}", author)
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone using milestone_title param' do
+ get api("/issues?milestone_title=#{no_milestone_title}", author)
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns an array of issues found by iids' do
+ get api('/issues', user), params: { iids: [closed_issue.iid] }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an empty array if iid does not exist' do
+ get api('/issues', user), params: { iids: [0] }
+
+ expect_paginated_array_response([])
+ end
+
+ context 'without sort params' do
+ it 'sorts by created_at descending by default' do
+ get api('/issues', user)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ context 'with 2 issues with same created_at' do
+ let!(:closed_issue2) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: closed_issue.created_at,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+
+ it 'page breaks first page correctly' do
+ get api('/issues?per_page=2', user)
+
+ expect_paginated_array_response([issue.id, closed_issue2.id])
+ end
+
+ it 'page breaks second page correctly' do
+ get api('/issues?per_page=2&page=2', user)
+
+ expect_paginated_array_response([closed_issue.id])
+ end
+ end
+ end
+
+ it 'sorts ascending when requested' do
+ get api('/issues?sort=asc', user)
+
+ expect_paginated_array_response([closed_issue.id, issue.id])
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get api('/issues?order_by=updated_at', user)
+
+ issue.touch(:updated_at)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api('/issues?order_by=updated_at&sort=asc', user)
+
+ issue.touch(:updated_at)
+
+ expect_paginated_array_response([closed_issue.id, issue.id])
+ end
+
+ it 'matches V4 response schema' do
+ get api('/issues', user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/issues')
+ end
+
+ it 'returns a related merge request count of 0 if there are no related merge requests' do
+ get api('/issues', user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/issues')
+ expect(json_response.first).to include('merge_requests_count' => 0)
+ end
+
+ it 'returns a related merge request count > 0 if there are related merge requests' do
+ create(:merge_requests_closing_issues, issue: issue)
+
+ get api('/issues', user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/issues')
+ expect(json_response.first).to include('merge_requests_count' => 1)
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'sort does not affect statistics ' do
+ let(:params) { { state: :opened, order_by: 'updated_at' } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+ end
+
+ context 'filtering by assignee_username' do
+ let(:another_assignee) { create(:assignee) }
+ let!(:issue1) { create(:issue, author: user2, project: project, created_at: 3.days.ago) }
+ let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) }
+ let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) }
+
+ it 'returns issues with by assignee_username' do
+ get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([confidential_issue.id, issue3.id])
+ end
+
+ it 'returns issues by assignee_username as string' do
+ get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([confidential_issue.id, issue3.id])
+ end
+
+ it 'returns error when multiple assignees are passed' do
+ get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("allows one value, but found 2")
+ end
+
+ it 'returns error when assignee_username and assignee_id are passed together' do
+ get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("mutually exclusive")
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/issues/:issue_iid' do
+ it 'rejects a non member from deleting an issue' do
+ delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'rejects a developer from deleting an issue' do
+ delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ context 'when the user is project owner' do
+ let(:owner) { create(:user) }
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ it 'deletes the issue if an admin requests it' do
+ delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) }
+ end
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ delete api("/projects/#{project.id}/issues/123", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ it 'returns 404 when using the issue ID instead of IID' do
+ delete api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe 'time tracking endpoints' do
+ let(:issuable) { issue }
+
+ include_examples 'time tracking endpoints', 'issue'
+ end
+end
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
new file mode 100644
index 00000000000..b74e8867310
--- /dev/null
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -0,0 +1,549 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Issues do
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 3.hours.ago,
+ closed_at: 1.hour.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignees: [assignee],
+ created_at: generate(:past_time),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+ set(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ set(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { 'None' }
+ let(:any_milestone_title) { 'Any' }
+
+ before(:all) do
+ project.add_reporter(user)
+ project.add_guest(guest)
+ end
+
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ end
+
+ describe 'POST /projects/:id/issues' do
+ context 'support for deprecated assignee_id' do
+ it 'creates a new project issue' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', assignee_id: user2.id }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ it 'creates a new project issue when assignee_id is empty' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', assignee_id: '' }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['assignee']).to be_nil
+ end
+ end
+
+ context 'single assignee restrictions' do
+ it 'creates a new project issue with no more than one assignee' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', assignee_ids: [user2.id, guest.id] }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['assignees'].count).to eq(1)
+ end
+ end
+
+ context 'user does not have permissions to create issue' do
+ let(:not_member) { create(:user) }
+
+ before do
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'renders 403' do
+ post api("/projects/#{project.id}/issues", not_member), params: { title: 'new issue' }
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'an internal ID is provided' do
+ context 'by an admin' do
+ it 'sets the internal ID on the new issue' do
+ post api("/projects/#{project.id}/issues", admin),
+ params: { title: 'new issue', iid: 9001 }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['iid']).to eq 9001
+ end
+ end
+
+ context 'by an owner' do
+ it 'sets the internal ID on the new issue' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', iid: 9001 }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['iid']).to eq 9001
+ end
+ end
+
+ context 'by a group owner' do
+ let(:group) { create(:group) }
+ let(:group_project) { create(:project, :public, namespace: group) }
+
+ it 'sets the internal ID on the new issue' do
+ group.add_owner(user2)
+ post api("/projects/#{group_project.id}/issues", user2),
+ params: { title: 'new issue', iid: 9001 }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['iid']).to eq 9001
+ end
+ end
+
+ context 'by another user' do
+ it 'ignores the given internal ID' do
+ post api("/projects/#{project.id}/issues", user2),
+ params: { title: 'new issue', iid: 9001 }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['iid']).not_to eq 9001
+ end
+ end
+ end
+
+ it 'creates a new project issue' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', labels: 'label, label2', weight: 3, assignee_ids: [user2.id] }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['confidential']).to be_falsy
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ it 'creates a new project issue with labels param as array' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['confidential']).to be_falsy
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ it 'creates a new confidential project issue' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', confidential: true }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a new confidential project issue with a different param' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', confidential: 'y' }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a public issue when confidential param is false' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', confidential: false }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a public issue when confidential param is invalid' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', confidential: 'foo' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+
+ it 'returns a 400 bad request if title not given' do
+ post api("/projects/#{project.id}/issues", user), params: { labels: 'label, label2' }
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'allows special label names' do
+ post api("/projects/#{project.id}/issues", user),
+ params: {
+ title: 'new issue',
+ labels: 'label, label?, label&foo, ?, &'
+ }
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'allows special label names with labels param as array' do
+ post api("/projects/#{project.id}/issues", user),
+ params: {
+ title: 'new issue',
+ labels: ['label', 'label?', 'label&foo, ?, &']
+ }
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'returns 400 if title is too long' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'g' * 256 }
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+
+ context 'resolving discussions' do
+ let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
+ let(:merge_request) { discussion.noteable }
+ let(:project) { merge_request.source_project }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'resolving all discussions in a merge request' do
+ before do
+ post api("/projects/#{project.id}/issues", user),
+ params: {
+ title: 'New Issue',
+ merge_request_to_resolve_discussions_of: merge_request.iid
+ }
+ end
+
+ it_behaves_like 'creating an issue resolving discussions through the API'
+ end
+
+ context 'resolving a single discussion' do
+ before do
+ post api("/projects/#{project.id}/issues", user),
+ params: {
+ title: 'New Issue',
+ merge_request_to_resolve_discussions_of: merge_request.iid,
+ discussion_to_resolve: discussion.id
+ }
+ end
+
+ it_behaves_like 'creating an issue resolving discussions through the API'
+ end
+ end
+
+ context 'with due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', due_date: due_date }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
+ context 'setting created_at' do
+ let(:creation_time) { 2.weeks.ago }
+ let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } }
+
+ context 'by an admin' do
+ it 'sets the creation time on the new issue' do
+ post api("/projects/#{project.id}/issues", admin), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'by a project owner' do
+ it 'sets the creation time on the new issue' do
+ post api("/projects/#{project.id}/issues", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'by a group owner' do
+ it 'sets the creation time on the new issue' do
+ group = create(:group)
+ group_project = create(:project, :public, namespace: group)
+ group.add_owner(user2)
+ post api("/projects/#{group_project.id}/issues", user2), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'by another user' do
+ it 'ignores the given creation time' do
+ post api("/projects/#{project.id}/issues", user2), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(Time.parse(json_response['created_at'])).not_to be_like_time(creation_time)
+ end
+ end
+ end
+
+ context 'the user can only read the issue' do
+ it 'cannot create new labels' do
+ expect do
+ post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: 'label, label2' }
+ end.not_to change { project.labels.count }
+ end
+
+ it 'cannot create new labels with labels param as array' do
+ expect do
+ post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) }
+ end.not_to change { project.labels.count }
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/issues with spam filtering' do
+ before do
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
+ end
+
+ let(:params) do
+ {
+ title: 'new issue',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it 'does not create a new project issue' do
+ expect { post api("/projects/#{project.id}/issues", user), params: params }.not_to change(Issue, :count)
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq({ 'error' => 'Spam detected' })
+
+ spam_logs = SpamLog.all
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('new issue')
+ expect(spam_logs[0].description).to eq('content here')
+ expect(spam_logs[0].user).to eq(user)
+ expect(spam_logs[0].noteable_type).to eq('Issue')
+ end
+ end
+
+ describe '/projects/:id/issues/:issue_iid/move' do
+ let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
+ let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
+
+ it 'moves an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ params: { to_project_id: target_project.id }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['project_id']).to eq(target_project.id)
+ end
+
+ context 'when source and target projects are the same' do
+ it 'returns 400 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ params: { to_project_id: project.id }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
+ end
+ end
+
+ context 'when the user does not have the permission to move issues' do
+ it 'returns 400 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ params: { to_project_id: target_project2.id }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
+ end
+ end
+
+ it 'moves the issue to another namespace if I am admin' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
+ params: { to_project_id: target_project2.id }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['project_id']).to eq(target_project2.id)
+ end
+
+ context 'when using the issue ID instead of iid' do
+ it 'returns 404 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ params: { to_project_id: target_project.id }
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Issue Not Found')
+ end
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/123/move", user),
+ params: { to_project_id: target_project.id }
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Issue Not Found')
+ end
+ end
+
+ context 'when source project does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post api("/projects/0/issues/#{issue.iid}/move", user),
+ params: { to_project_id: target_project.id }
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'when target project does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ params: { to_project_id: 0 }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST :id/issues/:issue_iid/subscribe' do
+ it 'subscribes to an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
+
+ expect(response).to have_gitlab_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ post api("/projects/#{project.id}/issues/123/subscribe", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the issue ID is used instead of the iid' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe 'POST :id/issues/:issue_id/unsubscribe' do
+ it 'unsubscribes from an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
+
+ expect(response).to have_gitlab_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ post api("/projects/#{project.id}/issues/123/unsubscribe", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if using the issue ID instead of iid' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
new file mode 100644
index 00000000000..267cba93713
--- /dev/null
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -0,0 +1,392 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Issues do
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 3.hours.ago,
+ closed_at: 1.hour.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignees: [assignee],
+ created_at: generate(:past_time),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+ set(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ set(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { 'None' }
+ let(:any_milestone_title) { 'Any' }
+
+ before(:all) do
+ project.add_reporter(user)
+ project.add_guest(guest)
+ end
+
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid to update only title' do
+ it 'updates a project issue' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'returns 404 error if issue iid not found' do
+ put api("/projects/#{project.id}/issues/44444", user),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 error if issue id is used instead of the iid' do
+ put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'allows special label names' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: {
+ title: 'updated title',
+ labels: 'label, label?, label&foo, ?, &'
+ }
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'allows special label names with labels param as array' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: {
+ title: 'updated title',
+ labels: ['label', 'label?', 'label&foo, ?, &']
+ }
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ context 'confidential issues' do
+ it 'returns 403 for non project members' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'returns 403 for project members with guest role' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'updates a confidential issue for project members' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'updates a confidential issue for author' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'updates a confidential issue for admin' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'sets an issue to confidential' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { confidential: true }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'makes a confidential issue public' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
+ params: { confidential: false }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'does not update a confidential issue with wrong confidential flag' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
+ params: { confidential: 'foo' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
+ let(:params) do
+ {
+ title: 'updated title',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it 'does not create a new project issue' do
+ allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
+ allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
+
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq({ 'error' => 'Spam detected' })
+
+ spam_logs = SpamLog.all
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('updated title')
+ expect(spam_logs[0].description).to eq('content here')
+ expect(spam_logs[0].user).to eq(user)
+ expect(spam_logs[0].noteable_type).to eq('Issue')
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
+ context 'support for deprecated assignee_id' do
+ it 'removes assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { assignee_id: 0 }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['assignee']).to be_nil
+ end
+
+ it 'updates an issue with new assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { assignee_id: user2.id }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ end
+ end
+
+ it 'removes assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { assignee_ids: [0] }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['assignees']).to be_empty
+ end
+
+ it 'updates an issue with new assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { assignee_ids: [user2.id] }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ context 'single assignee restrictions' do
+ it 'updates an issue with several assignees but only one has been applied' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { assignee_ids: [user2.id, guest.id] }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['assignees'].size).to eq(1)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
+ let!(:label) { create(:label, title: 'dummy', project: project) }
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+
+ it 'does not update labels if not present' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to eq([label.title])
+ end
+
+ it 'removes all labels and touches the record' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: '' }
+ end
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to eq([])
+ expect(json_response['updated_at']).to be > Time.now
+ end
+
+ it 'removes all labels and touches the record with labels param as array' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: [''] }
+ end
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to eq([])
+ expect(json_response['updated_at']).to be > Time.now
+ end
+
+ it 'updates labels and touches the record' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: 'foo,bar' }
+ end
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to include 'foo'
+ expect(json_response['labels']).to include 'bar'
+ expect(json_response['updated_at']).to be > Time.now
+ end
+
+ it 'updates labels and touches the record with labels param as array' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: %w(foo bar) }
+ end
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to include 'foo'
+ expect(json_response['labels']).to include 'bar'
+ expect(json_response['updated_at']).to be > Time.now
+ end
+
+ it 'allows special label names' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label:foo'
+ expect(json_response['labels']).to include 'label-bar'
+ expect(json_response['labels']).to include 'label_bar'
+ expect(json_response['labels']).to include 'label/bar'
+ expect(json_response['labels']).to include 'label?bar'
+ expect(json_response['labels']).to include 'label&bar'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'allows special label names with labels param as array' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] }
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label:foo'
+ expect(json_response['labels']).to include 'label-bar'
+ expect(json_response['labels']).to include 'label_bar'
+ expect(json_response['labels']).to include 'label/bar'
+ expect(json_response['labels']).to include 'label?bar'
+ expect(json_response['labels']).to include 'label&bar'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'returns 400 if title is too long' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { title: 'g' * 256 }
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid to update state and label' do
+ it 'updates a project issue' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: 'label2', state_event: 'close' }
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['labels']).to include 'label2'
+ expect(json_response['state']).to eq 'closed'
+ end
+
+ it 'reopens a project isssue' do
+ put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), params: { state_event: 'reopen' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['state']).to eq 'opened'
+ end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the update date to be set' do
+ update_time = 2.weeks.ago
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: 'label3', state_event: 'close', updated_at: update_time }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to include 'label3'
+ expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { due_date: due_date }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
deleted file mode 100644
index 0fa34688371..00000000000
--- a/spec/requests/api/issues_spec.rb
+++ /dev/null
@@ -1,2265 +0,0 @@
-require 'spec_helper'
-
-describe API::Issues do
- set(:user) { create(:user) }
- set(:project) do
- create(:project, :public, creator_id: user.id, namespace: user.namespace)
- end
-
- let(:user2) { create(:user) }
- let(:non_member) { create(:user) }
- set(:guest) { create(:user) }
- set(:author) { create(:author) }
- set(:assignee) { create(:assignee) }
- let(:admin) { create(:user, :admin) }
- let(:issue_title) { 'foo' }
- let(:issue_description) { 'closed' }
- let!(:closed_issue) do
- create :closed_issue,
- author: user,
- assignees: [user],
- project: project,
- state: :closed,
- milestone: milestone,
- created_at: generate(:past_time),
- updated_at: 3.hours.ago,
- closed_at: 1.hour.ago
- end
- let!(:confidential_issue) do
- create :issue,
- :confidential,
- project: project,
- author: author,
- assignees: [assignee],
- created_at: generate(:past_time),
- updated_at: 2.hours.ago
- end
- let!(:issue) do
- create :issue,
- author: user,
- assignees: [user],
- project: project,
- milestone: milestone,
- created_at: generate(:past_time),
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description
- end
- set(:label) do
- create(:label, title: 'label', color: '#FFAABB', project: project)
- end
- let!(:label_link) { create(:label_link, label: label, target: issue) }
- let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
- set(:empty_milestone) do
- create(:milestone, title: '2.0.0', project: project)
- end
- let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
-
- let(:no_milestone_title) { "None" }
- let(:any_milestone_title) { "Any" }
-
- before(:all) do
- project.add_reporter(user)
- project.add_guest(guest)
- end
-
- describe "GET /issues" do
- context "when unauthenticated" do
- it "returns an array of all issues" do
- get api("/issues"), params: { scope: 'all' }
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- end
-
- it "returns authentication error without any scope" do
- get api("/issues")
-
- expect(response).to have_http_status(401)
- end
-
- it "returns authentication error when scope is assigned-to-me" do
- get api("/issues"), params: { scope: 'assigned-to-me' }
-
- expect(response).to have_http_status(401)
- end
-
- it "returns authentication error when scope is created-by-me" do
- get api("/issues"), params: { scope: 'created-by-me' }
-
- expect(response).to have_http_status(401)
- end
- end
-
- context "when authenticated" do
- it "returns an array of issues" do
- get api("/issues", user)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- expect(json_response.first['title']).to eq(issue.title)
- expect(json_response.last).to have_key('web_url')
- end
-
- it 'returns an array of closed issues' do
- get api('/issues', user), params: { state: :closed }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an array of opened issues' do
- get api('/issues', user), params: { state: :opened }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of all issues' do
- get api('/issues', user), params: { state: :all }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns issues assigned to me' do
- issue2 = create(:issue, assignees: [user2], project: project)
-
- get api('/issues', user2), params: { scope: 'assigned_to_me' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues assigned to me (kebab-case)' do
- issue2 = create(:issue, assignees: [user2], project: project)
-
- get api('/issues', user2), params: { scope: 'assigned-to-me' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues authored by the given author id' do
- issue2 = create(:issue, author: user2, project: project)
-
- get api('/issues', user), params: { author_id: user2.id, scope: 'all' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues assigned to the given assignee id' do
- issue2 = create(:issue, assignees: [user2], project: project)
-
- get api('/issues', user), params: { assignee_id: user2.id, scope: 'all' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues authored by the given author id and assigned to the given assignee id' do
- issue2 = create(:issue, author: user2, assignees: [user2], project: project)
-
- get api('/issues', user), params: { author_id: user2.id, assignee_id: user2.id, scope: 'all' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues with no assignee' do
- issue2 = create(:issue, author: user2, project: project)
-
- get api('/issues', user), params: { assignee_id: 0, scope: 'all' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues with no assignee' do
- issue2 = create(:issue, author: user2, project: project)
-
- get api('/issues', user), params: { assignee_id: 'None', scope: 'all' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues with any assignee' do
- # This issue without assignee should not be returned
- create(:issue, author: user2, project: project)
-
- get api('/issues', user), params: { assignee_id: 'Any', scope: 'all' }
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'returns only confidential issues' do
- get api('/issues', user), params: { confidential: true, scope: 'all' }
-
- expect_paginated_array_response(confidential_issue.id)
- end
-
- it 'returns only public issues' do
- get api('/issues', user), params: { confidential: false }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns issues reacted by the authenticated user' do
- issue2 = create(:issue, project: project, author: user, assignees: [user])
- create(:award_emoji, awardable: issue2, user: user2, name: 'star')
- create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup')
-
- get api('/issues', user2), params: { my_reaction_emoji: 'Any', scope: 'all' }
-
- expect_paginated_array_response([issue2.id, issue.id])
- end
-
- it 'returns issues not reacted by the authenticated user' do
- issue2 = create(:issue, project: project, author: user, assignees: [user])
- create(:award_emoji, awardable: issue2, user: user2, name: 'star')
-
- get api('/issues', user2), params: { my_reaction_emoji: 'None', scope: 'all' }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns issues matching given search string for title' do
- get api("/issues", user), params: { search: issue.title }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns issues matching given search string for title and scoped in title' do
- get api("/issues", user), params: { search: issue.title, in: 'title' }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an empty array if no issue matches given search string for title and scoped in description' do
- get api("/issues", user), params: { search: issue.title, in: 'description' }
-
- expect_paginated_array_response([])
- end
-
- it 'returns issues matching given search string for description' do
- get api("/issues", user), params: { search: issue.description }
-
- expect_paginated_array_response(issue.id)
- end
-
- context 'filtering before a specific date' do
- let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) }
-
- it 'returns issues created before a specific date' do
- get api('/issues?created_before=2000-01-02T00:00:00.060Z', user)
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues updated before a specific date' do
- get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user)
-
- expect_paginated_array_response(issue2.id)
- end
- end
-
- context 'filtering after a specific date' do
- let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) }
-
- it 'returns issues created after a specific date' do
- get api("/issues?created_after=#{issue2.created_at}", user)
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues updated after a specific date' do
- get api("/issues?updated_after=#{issue2.updated_at}", user)
-
- expect_paginated_array_response(issue2.id)
- end
- end
-
- it 'returns an array of labeled issues' do
- get api('/issues', user), params: { labels: label.title }
-
- expect_paginated_array_response(issue.id)
- expect(json_response.first['labels']).to eq([label.title])
- end
-
- it 'returns an array of labeled issues with labels param as array' do
- get api('/issues', user), params: { labels: [label.title] }
-
- expect_paginated_array_response(issue.id)
- expect(json_response.first['labels']).to eq([label.title])
- end
-
- it 'returns an array of labeled issues when all labels matches' do
- label_b = create(:label, title: 'foo', project: project)
- label_c = create(:label, title: 'bar', project: project)
-
- create(:label_link, label: label_b, target: issue)
- create(:label_link, label: label_c, target: issue)
-
- get api('/issues', user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" }
-
- expect_paginated_array_response(issue.id)
- expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
- end
-
- it 'returns an array of labeled issues when all labels matches with labels param as array' do
- label_b = create(:label, title: 'foo', project: project)
- label_c = create(:label, title: 'bar', project: project)
-
- create(:label_link, label: label_b, target: issue)
- create(:label_link, label: label_c, target: issue)
-
- get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
-
- expect_paginated_array_response(issue.id)
- expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
- end
-
- it 'returns an empty array if no issue matches labels' do
- get api('/issues', user), params: { labels: 'foo,bar' }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if no issue matches labels with labels param as array' do
- get api('/issues', user), params: { labels: %w(foo bar) }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of labeled issues matching given state' do
- get api('/issues', user), params: { labels: label.title, state: :opened }
-
- expect_paginated_array_response(issue.id)
- expect(json_response.first['labels']).to eq([label.title])
- expect(json_response.first['state']).to eq('opened')
- end
-
- it 'returns an array of labeled issues matching given state with labels param as array' do
- get api('/issues', user), params: { labels: [label.title], state: :opened }
-
- expect_paginated_array_response(issue.id)
- expect(json_response.first['labels']).to eq([label.title])
- expect(json_response.first['state']).to eq('opened')
- end
-
- it 'returns an empty array if no issue matches labels and state filters' do
- get api('/issues', user), params: { labels: label.title, state: :closed }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of issues with any label' do
- get api('/issues', user), params: { labels: IssuesFinder::FILTER_ANY }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of issues with any label with labels param as array' do
- get api('/issues', user), params: { labels: [IssuesFinder::FILTER_ANY] }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of issues with no label' do
- get api('/issues', user), params: { labels: IssuesFinder::FILTER_NONE }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an array of issues with no label with labels param as array' do
- get api('/issues', user), params: { labels: [IssuesFinder::FILTER_NONE] }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an array of issues with no label when using the legacy No+Label filter' do
- get api('/issues', user), params: { labels: 'No Label' }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an array of issues with no label when using the legacy No+Label filter with labels param as array' do
- get api('/issues', user), params: { labels: ['No Label'] }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get api("/issues?milestone=#{empty_milestone.title}", user)
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if milestone does not exist' do
- get api("/issues?milestone=foo", user)
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of issues in given milestone' do
- get api("/issues?milestone=#{milestone.title}", user)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns an array of issues matching state in milestone' do
- get api("/issues?milestone=#{milestone.title}"\
- '&state=closed', user)
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an array of issues with no milestone' do
- get api("/issues?milestone=#{no_milestone_title}", author)
-
- expect_paginated_array_response(confidential_issue.id)
- end
-
- it 'returns an array of issues found by iids' do
- get api('/issues', user), params: { iids: [closed_issue.iid] }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an empty array if iid does not exist' do
- get api("/issues", user), params: { iids: [0] }
-
- expect_paginated_array_response([])
- end
-
- context 'without sort params' do
- it 'sorts by created_at descending by default' do
- get api('/issues', user)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- context 'with 2 issues with same created_at' do
- let!(:closed_issue2) do
- create :closed_issue,
- author: user,
- assignees: [user],
- project: project,
- milestone: milestone,
- created_at: closed_issue.created_at,
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description
- end
-
- it 'page breaks first page correctly' do
- get api('/issues?per_page=2', user)
-
- expect_paginated_array_response([issue.id, closed_issue2.id])
- end
-
- it 'page breaks second page correctly' do
- get api('/issues?per_page=2&page=2', user)
-
- expect_paginated_array_response([closed_issue.id])
- end
- end
- end
-
- it 'sorts ascending when requested' do
- get api('/issues?sort=asc', user)
-
- expect_paginated_array_response([closed_issue.id, issue.id])
- end
-
- it 'sorts by updated_at descending when requested' do
- get api('/issues?order_by=updated_at', user)
-
- issue.touch(:updated_at)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'sorts by updated_at ascending when requested' do
- get api('/issues?order_by=updated_at&sort=asc', user)
-
- issue.touch(:updated_at)
-
- expect_paginated_array_response([closed_issue.id, issue.id])
- end
-
- it 'matches V4 response schema' do
- get api('/issues', user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/issues')
- end
-
- it 'returns a related merge request count of 0 if there are no related merge requests' do
- get api('/issues', user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/issues')
- expect(json_response.first).to include('merge_requests_count' => 0)
- end
-
- it 'returns a related merge request count > 0 if there are related merge requests' do
- create(:merge_requests_closing_issues, issue: issue)
-
- get api('/issues', user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/issues')
- expect(json_response.first).to include('merge_requests_count' => 1)
- end
- end
- end
-
- describe "GET /groups/:id/issues" do
- let!(:group) { create(:group) }
- let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
- let!(:group_closed_issue) do
- create :closed_issue,
- author: user,
- assignees: [user],
- project: group_project,
- state: :closed,
- milestone: group_milestone,
- updated_at: 3.hours.ago,
- created_at: 1.day.ago
- end
- let!(:group_confidential_issue) do
- create :issue,
- :confidential,
- project: group_project,
- author: author,
- assignees: [assignee],
- updated_at: 2.hours.ago,
- created_at: 2.days.ago
- end
- let!(:group_issue) do
- create :issue,
- author: user,
- assignees: [user],
- project: group_project,
- milestone: group_milestone,
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description,
- created_at: 5.days.ago
- end
- let!(:group_label) do
- create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
- end
- let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
- let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
- let!(:group_empty_milestone) do
- create(:milestone, title: '4.0.0', project: group_project)
- end
- let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
-
- let(:base_url) { "/groups/#{group.id}/issues" }
-
- context 'when group has subgroups', :nested_groups do
- let(:subgroup_1) { create(:group, parent: group) }
- let(:subgroup_2) { create(:group, parent: subgroup_1) }
-
- let(:subgroup_1_project) { create(:project, namespace: subgroup_1) }
- let(:subgroup_2_project) { create(:project, namespace: subgroup_2) }
-
- let!(:issue_1) { create(:issue, project: subgroup_1_project) }
- let!(:issue_2) { create(:issue, project: subgroup_2_project) }
-
- before do
- group.add_developer(user)
- end
-
- it 'also returns subgroups projects issues' do
- get api(base_url, user)
-
- expect_paginated_array_response([issue_2.id, issue_1.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id])
- end
- end
-
- context 'when user is unauthenticated' do
- it 'lists all issues in public projects' do
- get api(base_url)
-
- expect_paginated_array_response([group_closed_issue.id, group_issue.id])
- end
- end
-
- context 'when user is a group member' do
- before do
- group_project.add_reporter(user)
- end
-
- it 'returns all group issues (including opened and closed)' do
- get api(base_url, admin)
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
- end
-
- it 'returns group issues without confidential issues for non project members' do
- get api(base_url, non_member), params: { state: :opened }
-
- expect_paginated_array_response(group_issue.id)
- end
-
- it 'returns group confidential issues for author' do
- get api(base_url, author), params: { state: :opened }
-
- expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
- end
-
- it 'returns group confidential issues for assignee' do
- get api(base_url, assignee), params: { state: :opened }
-
- expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
- end
-
- it 'returns group issues with confidential issues for project members' do
- get api(base_url, user), params: { state: :opened }
-
- expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
- end
-
- it 'returns group confidential issues for admin' do
- get api(base_url, admin), params: { state: :opened }
-
- expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
- end
-
- it 'returns only confidential issues' do
- get api(base_url, user), params: { confidential: true }
-
- expect_paginated_array_response(group_confidential_issue.id)
- end
-
- it 'returns only public issues' do
- get api(base_url, user), params: { confidential: false }
-
- expect_paginated_array_response([group_closed_issue.id, group_issue.id])
- end
-
- it 'returns an array of labeled group issues' do
- get api(base_url, user), params: { labels: group_label.title }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['labels']).to eq([group_label.title])
- end
-
- it 'returns an array of labeled group issues with labels param as array' do
- get api(base_url, user), params: { labels: [group_label.title] }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['labels']).to eq([group_label.title])
- end
-
- it 'returns an array of labeled group issues where all labels match' do
- get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of labeled group issues where all labels match with labels param as array' do
- get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] }
-
- expect_paginated_array_response([])
- end
-
- it 'returns issues matching given search string for title' do
- get api(base_url, user), params: { search: group_issue.title }
-
- expect_paginated_array_response(group_issue.id)
- end
-
- it 'returns issues matching given search string for description' do
- get api(base_url, user), params: { search: group_issue.description }
-
- expect_paginated_array_response(group_issue.id)
- end
-
- it 'returns an array of labeled issues when all labels matches' do
- label_b = create(:label, title: 'foo', project: group_project)
- label_c = create(:label, title: 'bar', project: group_project)
-
- create(:label_link, label: label_b, target: group_issue)
- create(:label_link, label: label_c, target: group_issue)
-
- get api(base_url, user), params: { labels: "#{group_label.title},#{label_b.title},#{label_c.title}" }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
- end
-
- it 'returns an array of labeled issues when all labels matches with labels param as array' do
- label_b = create(:label, title: 'foo', project: group_project)
- label_c = create(:label, title: 'bar', project: group_project)
-
- create(:label_link, label: label_b, target: group_issue)
- create(:label_link, label: label_c, target: group_issue)
-
- get api(base_url, user), params: { labels: [group_label.title, label_b.title, label_c.title] }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
- end
-
- it 'returns an array of issues found by iids' do
- get api(base_url, user), params: { iids: [group_issue.iid] }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
-
- it 'returns an empty array if iid does not exist' do
- get api(base_url, user), params: { iids: [0] }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if no group issue matches labels' do
- get api(base_url, user), params: { labels: 'foo,bar' }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of group issues with any label' do
- get api(base_url, user), params: { labels: IssuesFinder::FILTER_ANY }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
-
- it 'returns an array of group issues with any label with labels param as array' do
- get api(base_url, user), params: { labels: [IssuesFinder::FILTER_ANY] }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
-
- it 'returns an array of group issues with no label' do
- get api(base_url, user), params: { labels: IssuesFinder::FILTER_NONE }
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
- end
-
- it 'returns an array of group issues with no label with labels param as array' do
- get api(base_url, user), params: { labels: [IssuesFinder::FILTER_NONE] }
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get api(base_url, user), params: { milestone: group_empty_milestone.title }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if milestone does not exist' do
- get api(base_url, user), params: { milestone: 'foo' }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of issues in given milestone' do
- get api(base_url, user), params: { state: :opened, milestone: group_milestone.title }
-
- expect_paginated_array_response(group_issue.id)
- end
-
- it 'returns an array of issues matching state in milestone' do
- get api(base_url, user), params: { milestone: group_milestone.title, state: :closed }
-
- expect_paginated_array_response(group_closed_issue.id)
- end
-
- it 'returns an array of issues with no milestone' do
- get api(base_url, user), params: { milestone: no_milestone_title }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect_paginated_array_response(group_confidential_issue.id)
- end
-
- context 'without sort params' do
- it 'sorts by created_at descending by default' do
- get api(base_url, user)
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
- end
-
- context 'with 2 issues with same created_at' do
- let!(:group_issue2) do
- create :issue,
- author: user,
- assignees: [user],
- project: group_project,
- milestone: group_milestone,
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description,
- created_at: group_issue.created_at
- end
-
- it 'page breaks first page correctly' do
- get api("#{base_url}?per_page=3", user)
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id])
- end
-
- it 'page breaks second page correctly' do
- get api("#{base_url}?per_page=3&page=2", user)
-
- expect_paginated_array_response([group_issue.id])
- end
- end
- end
-
- it 'sorts ascending when requested' do
- get api("#{base_url}?sort=asc", user)
-
- expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
- end
-
- it 'sorts by updated_at descending when requested' do
- get api("#{base_url}?order_by=updated_at", user)
-
- group_issue.touch(:updated_at)
-
- expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
- end
-
- it 'sorts by updated_at ascending when requested' do
- get api(base_url, user), params: { order_by: :updated_at, sort: :asc }
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
- end
- end
- end
-
- describe "GET /projects/:id/issues" do
- let(:base_url) { "/projects/#{project.id}" }
-
- context 'when unauthenticated' do
- it 'returns public project issues' do
- get api("/projects/#{project.id}/issues")
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
- end
-
- it 'avoids N+1 queries' do
- get api("/projects/#{project.id}/issues", user)
-
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{project.id}/issues", user)
- end.count
-
- create_list(:issue, 3, project: project)
-
- expect do
- get api("/projects/#{project.id}/issues", user)
- end.not_to exceed_all_query_limit(control_count)
- end
-
- it 'returns 404 when project does not exist' do
- get api('/projects/1000/issues', non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 404 on private projects for other users" do
- private_project = create(:project, :private)
- create(:issue, project: private_project)
-
- get api("/projects/#{private_project.id}/issues", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns no issues when user has access to project but not issues' do
- restricted_project = create(:project, :public, :issues_private)
- create(:issue, project: restricted_project)
-
- get api("/projects/#{restricted_project.id}/issues", non_member)
-
- expect_paginated_array_response([])
- end
-
- it 'returns project issues without confidential issues for non project members' do
- get api("#{base_url}/issues", non_member)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns project issues without confidential issues for project members with guest role' do
- get api("#{base_url}/issues", guest)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns project confidential issues for author' do
- get api("#{base_url}/issues", author)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'returns only confidential issues' do
- get api("#{base_url}/issues", author), params: { confidential: true }
-
- expect_paginated_array_response(confidential_issue.id)
- end
-
- it 'returns only public issues' do
- get api("#{base_url}/issues", author), params: { confidential: false }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns project confidential issues for assignee' do
- get api("#{base_url}/issues", assignee)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'returns project issues with confidential issues for project members' do
- get api("#{base_url}/issues", user)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'returns project confidential issues for admin' do
- get api("#{base_url}/issues", admin)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'returns an array of labeled project issues' do
- get api("#{base_url}/issues", user), params: { labels: label.title }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of labeled project issues with labels param as array' do
- get api("#{base_url}/issues", user), params: { labels: [label.title] }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of labeled issues when all labels matches' do
- label_b = create(:label, title: 'foo', project: project)
- label_c = create(:label, title: 'bar', project: project)
-
- create(:label_link, label: label_b, target: issue)
- create(:label_link, label: label_c, target: issue)
-
- get api("#{base_url}/issues", user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of labeled issues when all labels matches with labels param as array' do
- label_b = create(:label, title: 'foo', project: project)
- label_c = create(:label, title: 'bar', project: project)
-
- create(:label_link, label: label_b, target: issue)
- create(:label_link, label: label_c, target: issue)
-
- get api("#{base_url}/issues", user), params: { labels: [label.title, label_b.title, label_c.title] }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns issues matching given search string for title' do
- get api("#{base_url}/issues?search=#{issue.title}", user)
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns issues matching given search string for description' do
- get api("#{base_url}/issues?search=#{issue.description}", user)
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of issues found by iids' do
- get api("#{base_url}/issues", user), params: { iids: [issue.iid] }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an empty array if iid does not exist' do
- get api("#{base_url}/issues", user), params: { iids: [0] }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if not all labels matches' do
- get api("#{base_url}/issues?labels=#{label.title},foo", user)
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of project issues with any label' do
- get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_ANY }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of project issues with any label with labels param as array' do
- get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_ANY] }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of project issues with no label' do
- get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_NONE }
-
- expect_paginated_array_response([confidential_issue.id, closed_issue.id])
- end
-
- it 'returns an array of project issues with no label with labels param as array' do
- get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_NONE] }
-
- expect_paginated_array_response([confidential_issue.id, closed_issue.id])
- end
-
- it 'returns an empty array if no project issue matches labels' do
- get api("#{base_url}/issues", user), params: { labels: 'foo,bar' }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get api("#{base_url}/issues", user), params: { milestone: empty_milestone.title }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if milestone does not exist' do
- get api("#{base_url}/issues", user), params: { milestone: :foo }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of issues in given milestone' do
- get api("#{base_url}/issues", user), params: { milestone: milestone.title }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns an array of issues matching state in milestone' do
- get api("#{base_url}/issues", user), params: { milestone: milestone.title, state: :closed }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an array of issues with no milestone' do
- get api("#{base_url}/issues", user), params: { milestone: no_milestone_title }
-
- expect_paginated_array_response(confidential_issue.id)
- end
-
- it 'returns an array of issues with any milestone' do
- get api("#{base_url}/issues", user), params: { milestone: any_milestone_title }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- context 'without sort params' do
- it 'sorts by created_at descending by default' do
- get api("#{base_url}/issues", user)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- context 'with 2 issues with same created_at' do
- let!(:closed_issue2) do
- create :closed_issue,
- author: user,
- assignees: [user],
- project: project,
- milestone: milestone,
- created_at: closed_issue.created_at,
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description
- end
-
- it 'page breaks first page correctly' do
- get api("#{base_url}/issues?per_page=3", user)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id])
- end
-
- it 'page breaks second page correctly' do
- get api("#{base_url}/issues?per_page=3&page=2", user)
-
- expect_paginated_array_response([closed_issue.id])
- end
- end
- end
-
- it 'sorts ascending when requested' do
- get api("#{base_url}/issues", user), params: { sort: :asc }
-
- expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
- end
-
- it 'sorts by updated_at descending when requested' do
- get api("#{base_url}/issues", user), params: { order_by: :updated_at }
-
- issue.touch(:updated_at)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'sorts by updated_at ascending when requested' do
- get api("#{base_url}/issues", user), params: { order_by: :updated_at, sort: :asc }
-
- expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
- end
- end
-
- describe "GET /projects/:id/issues/:issue_iid" do
- context 'when unauthenticated' do
- it 'returns public issues' do
- get api("/projects/#{project.id}/issues/#{issue.iid}")
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- it 'exposes known attributes' do
- get api("/projects/#{project.id}/issues/#{issue.iid}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['id']).to eq(issue.id)
- expect(json_response['iid']).to eq(issue.iid)
- expect(json_response['project_id']).to eq(issue.project.id)
- expect(json_response['title']).to eq(issue.title)
- expect(json_response['description']).to eq(issue.description)
- expect(json_response['state']).to eq(issue.state)
- expect(json_response['closed_at']).to be_falsy
- expect(json_response['created_at']).to be_present
- expect(json_response['updated_at']).to be_present
- expect(json_response['labels']).to eq(issue.label_names)
- expect(json_response['milestone']).to be_a Hash
- expect(json_response['assignees']).to be_a Array
- expect(json_response['assignee']).to be_a Hash
- expect(json_response['author']).to be_a Hash
- expect(json_response['confidential']).to be_falsy
- end
-
- it "exposes the 'closed_at' attribute" do
- get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['closed_at']).to be_present
- end
-
- context 'links exposure' do
- it 'exposes related resources full URIs' do
- get api("/projects/#{project.id}/issues/#{issue.iid}", user)
-
- links = json_response['_links']
-
- expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}")
- expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes")
- expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji")
- expect(links['project']).to end_with("/api/v4/projects/#{project.id}")
- end
- end
-
- it "returns a project issue by internal id" do
- get api("/projects/#{project.id}/issues/#{issue.iid}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(issue.title)
- expect(json_response['iid']).to eq(issue.iid)
- end
-
- it "returns 404 if issue id not found" do
- get api("/projects/#{project.id}/issues/54321", user)
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 404 if the issue ID is used" do
- get api("/projects/#{project.id}/issues/#{issue.id}", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- context 'confidential issues' do
- it "returns 404 for non project members" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 404 for project members with guest role" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns confidential issue for project members" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(confidential_issue.title)
- expect(json_response['iid']).to eq(confidential_issue.iid)
- end
-
- it "returns confidential issue for author" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(confidential_issue.title)
- expect(json_response['iid']).to eq(confidential_issue.iid)
- end
-
- it "returns confidential issue for assignee" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(confidential_issue.title)
- expect(json_response['iid']).to eq(confidential_issue.iid)
- end
-
- it "returns confidential issue for admin" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(confidential_issue.title)
- expect(json_response['iid']).to eq(confidential_issue.iid)
- end
- end
- end
-
- describe "POST /projects/:id/issues" do
- context 'support for deprecated assignee_id' do
- it 'creates a new project issue' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', assignee_id: user2.id }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['assignee']['name']).to eq(user2.name)
- expect(json_response['assignees'].first['name']).to eq(user2.name)
- end
-
- it 'creates a new project issue when assignee_id is empty' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', assignee_id: '' }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['assignee']).to be_nil
- end
- end
-
- context 'single assignee restrictions' do
- it 'creates a new project issue with no more than one assignee' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', assignee_ids: [user2.id, guest.id] }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['assignees'].count).to eq(1)
- end
- end
-
- context 'user does not have permissions to create issue' do
- let(:not_member) { create(:user) }
-
- before do
- project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
- end
-
- it 'renders 403' do
- post api("/projects/#{project.id}/issues", not_member), params: { title: 'new issue' }
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context 'an internal ID is provided' do
- context 'by an admin' do
- it 'sets the internal ID on the new issue' do
- post api("/projects/#{project.id}/issues", admin),
- params: { title: 'new issue', iid: 9001 }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['iid']).to eq 9001
- end
- end
-
- context 'by an owner' do
- it 'sets the internal ID on the new issue' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', iid: 9001 }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['iid']).to eq 9001
- end
- end
-
- context 'by a group owner' do
- let(:group) { create(:group) }
- let(:group_project) { create(:project, :public, namespace: group) }
-
- it 'sets the internal ID on the new issue' do
- group.add_owner(user2)
- post api("/projects/#{group_project.id}/issues", user2),
- params: { title: 'new issue', iid: 9001 }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['iid']).to eq 9001
- end
- end
-
- context 'by another user' do
- it 'ignores the given internal ID' do
- post api("/projects/#{project.id}/issues", user2),
- params: { title: 'new issue', iid: 9001 }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['iid']).not_to eq 9001
- end
- end
- end
-
- it 'creates a new project issue' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', labels: 'label, label2', weight: 3, assignee_ids: [user2.id] }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['description']).to be_nil
- expect(json_response['labels']).to eq(%w(label label2))
- expect(json_response['confidential']).to be_falsy
- expect(json_response['assignee']['name']).to eq(user2.name)
- expect(json_response['assignees'].first['name']).to eq(user2.name)
- end
-
- it 'creates a new project issue with labels param as array' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['description']).to be_nil
- expect(json_response['labels']).to eq(%w(label label2))
- expect(json_response['confidential']).to be_falsy
- expect(json_response['assignee']['name']).to eq(user2.name)
- expect(json_response['assignees'].first['name']).to eq(user2.name)
- end
-
- it 'creates a new confidential project issue' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', confidential: true }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_truthy
- end
-
- it 'creates a new confidential project issue with a different param' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', confidential: 'y' }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_truthy
- end
-
- it 'creates a public issue when confidential param is false' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', confidential: false }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_falsy
- end
-
- it 'creates a public issue when confidential param is invalid' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', confidential: 'foo' }
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['error']).to eq('confidential is invalid')
- end
-
- it "returns a 400 bad request if title not given" do
- post api("/projects/#{project.id}/issues", user), params: { labels: 'label, label2' }
- expect(response).to have_gitlab_http_status(400)
- end
-
- it 'allows special label names' do
- post api("/projects/#{project.id}/issues", user),
- params: {
- title: 'new issue',
- labels: 'label, label?, label&foo, ?, &'
- }
- expect(response.status).to eq(201)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- it 'allows special label names with labels param as array' do
- post api("/projects/#{project.id}/issues", user),
- params: {
- title: 'new issue',
- labels: ['label', 'label?', 'label&foo, ?, &']
- }
- expect(response.status).to eq(201)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- it 'returns 400 if title is too long' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'g' * 256 }
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['title']).to eq([
- 'is too long (maximum is 255 characters)'
- ])
- end
-
- context 'resolving discussions' do
- let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
- let(:merge_request) { discussion.noteable }
- let(:project) { merge_request.source_project }
-
- before do
- project.add_maintainer(user)
- end
-
- context 'resolving all discussions in a merge request' do
- before do
- post api("/projects/#{project.id}/issues", user),
- params: {
- title: 'New Issue',
- merge_request_to_resolve_discussions_of: merge_request.iid
- }
- end
-
- it_behaves_like 'creating an issue resolving discussions through the API'
- end
-
- context 'resolving a single discussion' do
- before do
- post api("/projects/#{project.id}/issues", user),
- params: {
- title: 'New Issue',
- merge_request_to_resolve_discussions_of: merge_request.iid,
- discussion_to_resolve: discussion.id
- }
- end
-
- it_behaves_like 'creating an issue resolving discussions through the API'
- end
- end
-
- context 'with due date' do
- it 'creates a new project issue' do
- due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
-
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', due_date: due_date }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['description']).to be_nil
- expect(json_response['due_date']).to eq(due_date)
- end
- end
-
- context 'setting created_at' do
- let(:creation_time) { 2.weeks.ago }
- let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } }
-
- context 'by an admin' do
- before do
- post api("/projects/#{project.id}/issues", admin), params: params
- end
-
- it 'sets the creation time on the new issue' do
- expect(response).to have_gitlab_http_status(201)
- expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
- end
-
- it 'sets the system notes timestamp based on creation time' do
- issue = Issue.find(json_response['id'])
-
- expect(issue.resource_label_events.last.created_at).to be_like_time(creation_time)
- end
- end
-
- context 'by a project owner' do
- it 'sets the creation time on the new issue' do
- post api("/projects/#{project.id}/issues", user), params: params
-
- expect(response).to have_gitlab_http_status(201)
- expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
- end
- end
-
- context 'by a group owner' do
- it 'sets the creation time on the new issue' do
- group = create(:group)
- group_project = create(:project, :public, namespace: group)
- group.add_owner(user2)
- post api("/projects/#{group_project.id}/issues", user2), params: params
-
- expect(response).to have_gitlab_http_status(201)
- expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
- end
- end
-
- context 'by another user' do
- it 'ignores the given creation time' do
- post api("/projects/#{project.id}/issues", user2), params: params
-
- expect(response).to have_gitlab_http_status(201)
- expect(Time.parse(json_response['created_at'])).not_to be_like_time(creation_time)
- end
- end
- end
-
- context 'the user can only read the issue' do
- it 'cannot create new labels' do
- expect do
- post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: 'label, label2' }
- end.not_to change { project.labels.count }
- end
-
- it 'cannot create new labels with labels param as array' do
- expect do
- post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) }
- end.not_to change { project.labels.count }
- end
- end
- end
-
- describe 'POST /projects/:id/issues with spam filtering' do
- before do
- allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
- end
-
- let(:params) do
- {
- title: 'new issue',
- description: 'content here',
- labels: 'label, label2'
- }
- end
-
- it "does not create a new project issue" do
- expect { post api("/projects/#{project.id}/issues", user), params: params }.not_to change(Issue, :count)
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq({ "error" => "Spam detected" })
-
- spam_logs = SpamLog.all
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('new issue')
- expect(spam_logs[0].description).to eq('content here')
- expect(spam_logs[0].user).to eq(user)
- expect(spam_logs[0].noteable_type).to eq('Issue')
- end
- end
-
- describe "PUT /projects/:id/issues/:issue_iid to update only title" do
- it "updates a project issue" do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['title']).to eq('updated title')
- end
-
- it "returns 404 error if issue iid not found" do
- put api("/projects/#{project.id}/issues/44444", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 404 error if issue id is used instead of the iid" do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'allows special label names' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: {
- title: 'updated title',
- labels: 'label, label?, label&foo, ?, &'
- }
-
- expect(response.status).to eq(200)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- it 'allows special label names with labels param as array' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: {
- title: 'updated title',
- labels: ['label', 'label?', 'label&foo, ?, &']
- }
-
- expect(response.status).to eq(200)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- context 'confidential issues' do
- it "returns 403 for non project members" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(403)
- end
-
- it "returns 403 for project members with guest role" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(403)
- end
-
- it "updates a confidential issue for project members" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('updated title')
- end
-
- it "updates a confidential issue for author" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('updated title')
- end
-
- it "updates a confidential issue for admin" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('updated title')
- end
-
- it 'sets an issue to confidential' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { confidential: true }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['confidential']).to be_truthy
- end
-
- it 'makes a confidential issue public' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
- params: { confidential: false }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['confidential']).to be_falsy
- end
-
- it 'does not update a confidential issue with wrong confidential flag' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
- params: { confidential: 'foo' }
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['error']).to eq('confidential is invalid')
- end
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
- let(:params) do
- {
- title: 'updated title',
- description: 'content here',
- labels: 'label, label2'
- }
- end
-
- it "does not create a new project issue" do
- allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
- allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
-
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq({ "error" => "Spam detected" })
-
- spam_logs = SpamLog.all
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('updated title')
- expect(spam_logs[0].description).to eq('content here')
- expect(spam_logs[0].user).to eq(user)
- expect(spam_logs[0].noteable_type).to eq('Issue')
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
- context 'support for deprecated assignee_id' do
- it 'removes assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_id: 0 }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['assignee']).to be_nil
- end
-
- it 'updates an issue with new assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_id: user2.id }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['assignee']['name']).to eq(user2.name)
- end
- end
-
- it 'removes assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_ids: [0] }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['assignees']).to be_empty
- end
-
- it 'updates an issue with new assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_ids: [user2.id] }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['assignees'].first['name']).to eq(user2.name)
- end
-
- context 'single assignee restrictions' do
- it 'updates an issue with several assignees but only one has been applied' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_ids: [user2.id, guest.id] }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['assignees'].size).to eq(1)
- end
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
- let!(:label) { create(:label, title: 'dummy', project: project) }
- let!(:label_link) { create(:label_link, label: label, target: issue) }
-
- it 'does not update labels if not present' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to eq([label.title])
- end
-
- it 'removes all labels and touches the record' do
- Timecop.travel(1.minute.from_now) do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: '' }
- end
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to eq([])
- expect(json_response['updated_at']).to be > Time.now
- end
-
- it 'removes all labels and touches the record with labels param as array' do
- Timecop.travel(1.minute.from_now) do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: [''] }
- end
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to eq([])
- expect(json_response['updated_at']).to be > Time.now
- end
-
- it 'updates labels and touches the record' do
- Timecop.travel(1.minute.from_now) do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'foo,bar' }
- end
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to include 'foo'
- expect(json_response['labels']).to include 'bar'
- expect(json_response['updated_at']).to be > Time.now
- end
-
- it 'updates labels and touches the record with labels param as array' do
- Timecop.travel(1.minute.from_now) do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: %w(foo bar) }
- end
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to include 'foo'
- expect(json_response['labels']).to include 'bar'
- expect(json_response['updated_at']).to be > Time.now
- end
-
- it 'allows special label names' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
- expect(response.status).to eq(200)
- expect(json_response['labels']).to include 'label:foo'
- expect(json_response['labels']).to include 'label-bar'
- expect(json_response['labels']).to include 'label_bar'
- expect(json_response['labels']).to include 'label/bar'
- expect(json_response['labels']).to include 'label?bar'
- expect(json_response['labels']).to include 'label&bar'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- it 'allows special label names with labels param as array' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] }
- expect(response.status).to eq(200)
- expect(json_response['labels']).to include 'label:foo'
- expect(json_response['labels']).to include 'label-bar'
- expect(json_response['labels']).to include 'label_bar'
- expect(json_response['labels']).to include 'label/bar'
- expect(json_response['labels']).to include 'label?bar'
- expect(json_response['labels']).to include 'label&bar'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- it 'returns 400 if title is too long' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { title: 'g' * 256 }
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['title']).to eq([
- 'is too long (maximum is 255 characters)'
- ])
- end
- end
-
- describe "PUT /projects/:id/issues/:issue_iid to update state and label" do
- it "updates a project issue" do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'label2', state_event: "close" }
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['labels']).to include 'label2'
- expect(json_response['state']).to eq "closed"
- end
-
- it 'reopens a project isssue' do
- put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), params: { state_event: 'reopen' }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['state']).to eq 'opened'
- end
-
- context 'when an admin or owner makes the request' do
- it 'accepts the update date to be set' do
- update_time = 2.weeks.ago
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'label3', state_event: 'close', updated_at: update_time }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to include 'label3'
- expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
- end
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
- it 'creates a new project issue' do
- due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
-
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { due_date: due_date }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['due_date']).to eq(due_date)
- end
- end
-
- describe "DELETE /projects/:id/issues/:issue_iid" do
- it "rejects a non member from deleting an issue" do
- delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
- expect(response).to have_gitlab_http_status(403)
- end
-
- it "rejects a developer from deleting an issue" do
- delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
- expect(response).to have_gitlab_http_status(403)
- end
-
- context "when the user is project owner" do
- let(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
-
- it "deletes the issue if an admin requests it" do
- delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
-
- expect(response).to have_gitlab_http_status(204)
- end
-
- it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) }
- end
- end
-
- context 'when issue does not exist' do
- it 'returns 404 when trying to move an issue' do
- delete api("/projects/#{project.id}/issues/123", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- it 'returns 404 when using the issue ID instead of IID' do
- delete api("/projects/#{project.id}/issues/#{issue.id}", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe '/projects/:id/issues/:issue_iid/move' do
- let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
- let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
-
- it 'moves an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
- params: { to_project_id: target_project.id }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['project_id']).to eq(target_project.id)
- end
-
- context 'when source and target projects are the same' do
- it 'returns 400 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
- params: { to_project_id: project.id }
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
- end
- end
-
- context 'when the user does not have the permission to move issues' do
- it 'returns 400 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
- params: { to_project_id: target_project2.id }
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
- end
- end
-
- it 'moves the issue to another namespace if I am admin' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
- params: { to_project_id: target_project2.id }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['project_id']).to eq(target_project2.id)
- end
-
- context 'when using the issue ID instead of iid' do
- it 'returns 404 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
- params: { to_project_id: target_project.id }
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Issue Not Found')
- end
- end
-
- context 'when issue does not exist' do
- it 'returns 404 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/123/move", user),
- params: { to_project_id: target_project.id }
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Issue Not Found')
- end
- end
-
- context 'when source project does not exist' do
- it 'returns 404 when trying to move an issue' do
- post api("/projects/0/issues/#{issue.iid}/move", user),
- params: { to_project_id: target_project.id }
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
- end
- end
-
- context 'when target project does not exist' do
- it 'returns 404 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
- params: { to_project_id: 0 }
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'POST :id/issues/:issue_iid/subscribe' do
- it 'subscribes to an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['subscribed']).to eq(true)
- end
-
- it 'returns 304 if already subscribed' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
-
- expect(response).to have_gitlab_http_status(304)
- end
-
- it 'returns 404 if the issue is not found' do
- post api("/projects/#{project.id}/issues/123/subscribe", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 404 if the issue ID is used instead of the iid' do
- post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 404 if the issue is confidential' do
- post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe 'POST :id/issues/:issue_id/unsubscribe' do
- it 'unsubscribes from an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['subscribed']).to eq(false)
- end
-
- it 'returns 304 if not subscribed' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
-
- expect(response).to have_gitlab_http_status(304)
- end
-
- it 'returns 404 if the issue is not found' do
- post api("/projects/#{project.id}/issues/123/unsubscribe", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 404 if using the issue ID instead of iid' do
- post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 404 if the issue is confidential' do
- post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe 'time tracking endpoints' do
- let(:issuable) { issue }
-
- include_examples 'time tracking endpoints', 'issue'
- end
-
- describe 'GET :id/issues/:issue_iid/closed_by' do
- let(:merge_request) do
- create(:merge_request,
- :simple,
- author: user,
- source_project: project,
- target_project: project,
- description: "closes #{issue.to_reference}")
- end
-
- before do
- create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
- end
-
- context 'when unauthenticated' do
- it 'return public project issues' do
- get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
-
- expect_paginated_array_response(merge_request.id)
- end
- end
-
- it 'returns merge requests that will close issue on merge' do
- get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
-
- expect_paginated_array_response(merge_request.id)
- end
-
- context 'when no merge requests will close issue' do
- it 'returns empty array' do
- get api("/projects/#{project.id}/issues/#{closed_issue.iid}/closed_by", user)
-
- expect_paginated_array_response([])
- end
- end
-
- it "returns 404 when issue doesn't exists" do
- get api("/projects/#{project.id}/issues/0/closed_by", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe 'GET :id/issues/:issue_iid/related_merge_requests' do
- def get_related_merge_requests(project_id, issue_iid, user = nil)
- get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
- end
-
- def create_referencing_mr(user, project, issue)
- attributes = {
- author: user,
- source_project: project,
- target_project: project,
- source_branch: "master",
- target_branch: "test",
- description: "See #{issue.to_reference}"
- }
- create(:merge_request, attributes).tap do |merge_request|
- create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
- end
- end
-
- let!(:related_mr) { create_referencing_mr(user, project, issue) }
-
- context 'when unauthenticated' do
- it 'return list of referenced merge requests from issue' do
- get_related_merge_requests(project.id, issue.iid)
-
- expect_paginated_array_response(related_mr.id)
- end
-
- it 'renders 404 if project is not visible' do
- private_project = create(:project, :private)
- private_issue = create(:issue, project: private_project)
- create_referencing_mr(user, private_project, private_issue)
-
- get_related_merge_requests(private_project.id, private_issue.iid)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- it 'returns merge requests that mentioned a issue' do
- create(:merge_request,
- :simple,
- author: user,
- source_project: project,
- target_project: project,
- description: "Some description")
-
- get_related_merge_requests(project.id, issue.iid, user)
-
- expect_paginated_array_response(related_mr.id)
- end
-
- it 'returns merge requests cross-project wide' do
- project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
- merge_request = create_referencing_mr(user, project2, issue)
-
- get_related_merge_requests(project.id, issue.iid, user)
-
- expect_paginated_array_response([related_mr.id, merge_request.id])
- end
-
- it 'does not generate references to projects with no access' do
- private_project = create(:project, :private)
- create_referencing_mr(private_project.creator, private_project, issue)
-
- get_related_merge_requests(project.id, issue.iid, user)
-
- expect_paginated_array_response(related_mr.id)
- end
-
- context 'merge request closes an issue' do
- let!(:closing_issue_mr_rel) do
- create(:merge_requests_closing_issues, issue: issue, merge_request: related_mr)
- end
-
- it 'returns closing MR only once' do
- get_related_merge_requests(project.id, issue.iid, user)
-
- expect_paginated_array_response([related_mr.id])
- end
- end
-
- context 'no merge request mentioned a issue' do
- it 'returns empty array' do
- get_related_merge_requests(project.id, closed_issue.iid, user)
-
- expect_paginated_array_response([])
- end
- end
-
- it "returns 404 when issue doesn't exists" do
- get_related_merge_requests(project.id, 0, user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do
- let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
-
- context 'when unauthenticated' do
- it "returns unauthorized" do
- get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- it 'exposes known attributes' do
- get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
- expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
- expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
- end
-
- it "returns unauthorized for non-admin users" do
- get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- describe 'GET projects/:id/issues/:issue_iid/participants' do
- it_behaves_like 'issuable participants endpoint' do
- let(:entity) { issue }
- end
-
- it 'returns 404 if the issue is confidential' do
- post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index b331da1acba..4006e697a41 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -542,6 +542,30 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
+ context 'when job filtered by job_age' do
+ let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) }
+
+ context 'job is queued less than job_age parameter' do
+ let(:job_age) { 120 }
+
+ it 'gives 204' do
+ request_job(job_age: job_age)
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+ end
+
+ context 'job is queued more than job_age parameter' do
+ let(:job_age) { 30 }
+
+ it 'picks a job' do
+ request_job(job_age: job_age)
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+ end
+ end
+
context 'when job is made for branch' do
it 'sets tag as ref_type' do
request_job
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 49672591b3b..7d61ec9c4d8 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -414,6 +414,13 @@ describe API::Search do
expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(11)
end
+
+ it 'by ref' do
+ get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'This file is used in tests for ci_environments_status', ref: 'pages-deploy' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(1)
+ end
end
end
end
diff --git a/spec/serializers/test_case_entity_spec.rb b/spec/serializers/test_case_entity_spec.rb
index a55910f98bb..986c9feb07b 100644
--- a/spec/serializers/test_case_entity_spec.rb
+++ b/spec/serializers/test_case_entity_spec.rb
@@ -14,6 +14,7 @@ describe TestCaseEntity do
it 'contains correct test case details' do
expect(subject[:status]).to eq('success')
expect(subject[:name]).to eq('Test#sum when a is 1 and b is 3 returns summary')
+ expect(subject[:classname]).to eq('spec.test_spec')
expect(subject[:execution_time]).to eq(1.11)
end
end
@@ -24,6 +25,7 @@ describe TestCaseEntity do
it 'contains correct test case details' do
expect(subject[:status]).to eq('failed')
expect(subject[:name]).to eq('Test#sum when a is 2 and b is 2 returns summary')
+ expect(subject[:classname]).to eq('spec.test_spec')
expect(subject[:execution_time]).to eq(2.22)
end
end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index e24fe60f059..4f4776bbb27 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -285,7 +285,7 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'not a container repository factory'
end
- context 'disallow guest to delete images since regsitry 2.7' do
+ context 'disallow guest to delete images since registry 2.7' do
before do
project.add_guest(current_user)
end
diff --git a/spec/services/clusters/refresh_service_spec.rb b/spec/services/clusters/refresh_service_spec.rb
index 94c35228955..5bc8a709941 100644
--- a/spec/services/clusters/refresh_service_spec.rb
+++ b/spec/services/clusters/refresh_service_spec.rb
@@ -93,32 +93,14 @@ describe Clusters::RefreshService do
let(:group) { cluster.group }
let(:project) { create(:project, group: group) }
- context 'when ci_preparing_state feature flag is enabled' do
- include_examples 'does not create a kubernetes namespace'
-
- context 'when project already has kubernetes namespace' do
- before do
- create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
- end
-
- include_examples 'does not create a kubernetes namespace'
- end
- end
+ include_examples 'does not create a kubernetes namespace'
- context 'when ci_preparing_state feature flag is disabled' do
+ context 'when project already has kubernetes namespace' do
before do
- stub_feature_flags(ci_preparing_state: false)
+ create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
end
- include_examples 'creates a kubernetes namespace'
-
- context 'when project already has kubernetes namespace' do
- before do
- create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
- end
-
- include_examples 'does not create a kubernetes namespace'
- end
+ include_examples 'does not create a kubernetes namespace'
end
end
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
new file mode 100644
index 00000000000..4a2ec769116
--- /dev/null
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::BaseHooksService do
+ include RepoHelpers
+ include GitHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:service) { described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
+
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+ let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
+ let(:ref) { 'refs/tags/v1.1.0' }
+
+ describe 'with remote mirrors' do
+ class TestService < described_class
+ def commits
+ []
+ end
+ end
+
+ let(:project) { create(:project, :repository, :remote_mirror) }
+
+ subject { TestService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
+
+ before do
+ expect(subject).to receive(:execute_project_hooks)
+ end
+
+ context 'when remote mirror feature is enabled' do
+ it 'fails stuck remote mirrors' do
+ allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
+ expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'updates remote mirrors' do
+ expect(project).to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+
+ context 'when remote mirror feature is disabled' do
+ before do
+ stub_application_setting(mirror_available: false)
+ end
+
+ context 'with remote mirrors global setting overridden' do
+ before do
+ project.remote_mirror_available_overridden = true
+ end
+
+ it 'fails stuck remote mirrors' do
+ allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
+ expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'updates remote mirrors' do
+ expect(project).to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+
+ context 'without remote mirrors global setting overridden' do
+ before do
+ project.remote_mirror_available_overridden = false
+ end
+
+ it 'does not fails stuck remote mirrors' do
+ expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'does not updates remote mirrors' do
+ expect(project).not_to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
index 4895e762602..22faa996015 100644
--- a/spec/services/git/branch_hooks_service_spec.rb
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -18,6 +18,12 @@ describe Git::BranchHooksService do
described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
+ it 'update remote mirrors' do
+ expect(service).to receive(:update_remote_mirrors).and_call_original
+
+ service.execute
+ end
+
describe "Git Push Data" do
subject(:push_data) { service.execute }
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index ad21f710833..6e39fa6b3c0 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -17,72 +17,6 @@ describe Git::BranchPushService, services: true do
project.add_maintainer(user)
end
- describe 'with remote mirrors' do
- let(:project) { create(:project, :repository, :remote_mirror) }
-
- subject do
- described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
- end
-
- context 'when remote mirror feature is enabled' do
- it 'fails stuck remote mirrors' do
- allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
- expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
-
- subject.execute
- end
-
- it 'updates remote mirrors' do
- expect(project).to receive(:update_remote_mirrors)
-
- subject.execute
- end
- end
-
- context 'when remote mirror feature is disabled' do
- before do
- stub_application_setting(mirror_available: false)
- end
-
- context 'with remote mirrors global setting overridden' do
- before do
- project.remote_mirror_available_overridden = true
- end
-
- it 'fails stuck remote mirrors' do
- allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
- expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
-
- subject.execute
- end
-
- it 'updates remote mirrors' do
- expect(project).to receive(:update_remote_mirrors)
-
- subject.execute
- end
- end
-
- context 'without remote mirrors global setting overridden' do
- before do
- project.remote_mirror_available_overridden = false
- end
-
- it 'does not fails stuck remote mirrors' do
- expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!)
-
- subject.execute
- end
-
- it 'does not updates remote mirrors' do
- expect(project).not_to receive(:update_remote_mirrors)
-
- subject.execute
- end
- end
- end
- end
-
describe 'Push branches' do
subject do
execute_service(project, user, oldrev, newrev, ref)
diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb
index f4c02932f98..f5938a5c708 100644
--- a/spec/services/git/tag_hooks_service_spec.rb
+++ b/spec/services/git/tag_hooks_service_spec.rb
@@ -18,6 +18,12 @@ describe Git::TagHooksService, :service do
described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
+ it 'update remote mirrors' do
+ expect(service).to receive(:update_remote_mirrors).and_call_original
+
+ service.execute
+ end
+
describe 'System hooks' do
it 'Executes system hooks' do
push_data = service.execute
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index fce9eed8b08..6874a8a0929 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -3,11 +3,13 @@
require 'spec_helper'
describe Issues::CloseService do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user, email: "user@example.com") }
+ let(:user2) { create(:user, email: "user2@example.com") }
let(:guest) { create(:user) }
- let(:issue) { create(:issue, assignees: [user2], author: create(:user)) }
- let(:project) { issue.project }
+ let(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: create(:user)) }
+ let(:closing_merge_request) { create(:merge_request, source_project: project) }
+ let(:closing_commit) { create(:commit, project: project) }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
before do
@@ -39,7 +41,7 @@ describe Issues::CloseService do
.and_return(true)
expect(service).to receive(:close_issue)
- .with(issue, commit: nil, notifications: true, system_note: true)
+ .with(issue, closed_via: nil, notifications: true, system_note: true)
service.execute(issue)
end
@@ -57,6 +59,38 @@ describe Issues::CloseService do
end
describe '#close_issue' do
+ context "closed by a merge request" do
+ before do
+ perform_enqueued_jobs do
+ described_class.new(project, user).close_issue(issue, closed_via: closing_merge_request)
+ end
+ end
+
+ it 'mentions closure via a merge request' do
+ email = ActionMailer::Base.deliveries.last
+
+ expect(email.to.first).to eq(user2.email)
+ expect(email.subject).to include(issue.title)
+ expect(email.body.parts.map(&:body)).to all(include(closing_merge_request.to_reference))
+ end
+ end
+
+ context "closed by a commit" do
+ before do
+ perform_enqueued_jobs do
+ described_class.new(project, user).close_issue(issue, closed_via: closing_commit)
+ end
+ end
+
+ it 'mentions closure via a commit' do
+ email = ActionMailer::Base.deliveries.last
+
+ expect(email.to.first).to eq(user2.email)
+ expect(email.subject).to include(issue.title)
+ expect(email.body.parts.map(&:body)).to all(include(closing_commit.id))
+ end
+ end
+
context "valid params" do
before do
perform_enqueued_jobs do
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index a443e4588d9..7e2f03d1097 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -38,6 +38,32 @@ describe MergeRequests::RebaseService do
end
end
+ shared_examples 'sequence of failure and success' do
+ it 'properly clears the error message' do
+ allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
+
+ service.execute(merge_request)
+
+ expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR
+
+ allow(repository).to receive(:gitaly_operation_client).and_call_original
+
+ service.execute(merge_request)
+
+ expect(merge_request.reload.merge_error).to eq nil
+ end
+ end
+
+ it_behaves_like 'sequence of failure and success'
+
+ context 'with deprecated step rebase feature' do
+ before do
+ stub_feature_flags(two_step_rebase: false)
+ end
+
+ it_behaves_like 'sequence of failure and success'
+ end
+
context 'when unexpected error occurs' do
before do
allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index bd7a0c68766..f54f9200661 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -268,33 +268,6 @@ describe Projects::CreateService, '#execute' do
end
end
- context 'when group has kubernetes cluster' do
- let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) }
- let(:group) { group_cluster.group }
-
- let(:token) { 'aaaa' }
- let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) }
- let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) }
-
- before do
- group.add_owner(user)
-
- stub_feature_flags(ci_preparing_state: false)
- expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator)
- expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher)
- end
-
- it 'creates kubernetes namespace for the project' do
- project = create_project(user, opts.merge!(namespace_id: group.id))
-
- expect(project).to be_valid
-
- kubernetes_namespace = group_cluster.kubernetes_namespaces.first
- expect(kubernetes_namespace).to be_present
- expect(kubernetes_namespace.project).to eq(project)
- end
- end
-
context 'when there is an active service template' do
before do
create(:service, project: nil, template: true, active: true)
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 9acc3657fa9..a47c10d991a 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -73,33 +73,6 @@ describe Projects::TransferService do
shard_name: project.repository_storage
)
end
-
- context 'new group has a kubernetes cluster' do
- let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) }
- let(:group) { group_cluster.group }
-
- let(:token) { 'aaaa' }
- let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) }
- let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) }
-
- subject { transfer_project(project, user, group) }
-
- before do
- stub_feature_flags(ci_preparing_state: false)
- expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator)
- expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher)
- end
-
- it 'creates kubernetes namespace for the project' do
- subject
-
- expect(project.kubernetes_namespaces.count).to eq(1)
-
- kubernetes_namespace = group_cluster.kubernetes_namespaces.first
- expect(kubernetes_namespace).to be_present
- expect(kubernetes_namespace.project).to eq(project)
- end
- end
end
context 'when transfer fails' do
diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb
index 38f30a14409..8a139fafac2 100644
--- a/spec/support/helpers/features/notes_helpers.rb
+++ b/spec/support/helpers/features/notes_helpers.rb
@@ -25,12 +25,10 @@ module Spec
page.within('.js-main-target-form') do
filled_text = fill_in('note[note]', with: text)
- begin
- # Dismiss quick action prompt if it appears
- filled_text.parent.send_keys(:escape)
- rescue Selenium::WebDriver::Error::ElementNotInteractableError
- # It's fine if we can't escape when there's no prompt.
- end
+ # Wait for quick action prompt to load and then dismiss it with ESC
+ # because it may block the Preview button
+ wait_for_requests
+ filled_text.send_keys(:escape)
click_on('Preview')
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index f15944652fd..44ed9da25fc 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -197,3 +197,7 @@ module GraphqlHelpers
allow(GitlabSchema).to receive(:max_query_depth).with(any_args).and_return nil
end
end
+
+# This warms our schema, doing this as part of loading the helpers to avoid
+# duplicate loading error when Rails tries autoload the types.
+GitlabSchema.graphql_definition
diff --git a/spec/support/helpers/lets_encrypt_helpers.rb b/spec/support/helpers/lets_encrypt_helpers.rb
new file mode 100644
index 00000000000..7f0886b451c
--- /dev/null
+++ b/spec/support/helpers/lets_encrypt_helpers.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module LetsEncryptHelpers
+ def stub_lets_encrypt_client
+ client = instance_double('Acme::Client')
+
+ allow(client).to receive(:new_account)
+ allow(client).to receive(:terms_of_service).and_return(
+ "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf"
+ )
+
+ allow(Acme::Client).to receive(:new).with(
+ private_key: kind_of(OpenSSL::PKey::RSA),
+ directory: ::Gitlab::LetsEncrypt::Client::STAGING_DIRECTORY_URL
+ ).and_return(client)
+
+ client
+ end
+end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index dc902d373b8..06b5ecdf150 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -135,7 +135,7 @@ module TestEnv
def clean_gitlab_test_path
Dir[TMP_TEST_PATH].each do |entry|
- if File.basename(entry) =~ /\A(gitlab-(test|test_bare|test-fork|test-fork_bare))\z/
+ unless test_dirs.include?(File.basename(entry))
FileUtils.rm_rf(entry)
end
end
@@ -312,6 +312,18 @@ module TestEnv
private
+ # These are directories that should be preserved at cleanup time
+ def test_dirs
+ @test_dirs ||= %w[
+ gitaly
+ gitlab-shell
+ gitlab-test
+ gitlab-test_bare
+ gitlab-test-fork
+ gitlab-test-fork_bare
+ ]
+ end
+
def factory_repo_path
@factory_repo_path ||= Rails.root.join('tmp', 'tests', factory_repo_name)
end
diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions.rb
index 96f79081d26..c3132c41f5b 100644
--- a/spec/support/shared_examples/requests/api/discussions.rb
+++ b/spec/support/shared_examples/requests/api/discussions.rb
@@ -1,4 +1,4 @@
-shared_examples 'discussions API' do |parent_type, noteable_type, id_name, can_reply_to_invididual_notes: false|
+shared_examples 'discussions API' do |parent_type, noteable_type, id_name, can_reply_to_individual_notes: false|
describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do
it "returns an array of discussions" do
get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user)
@@ -144,7 +144,7 @@ shared_examples 'discussions API' do |parent_type, noteable_type, id_name, can_r
"discussions/#{note.discussion_id}/notes", user), params: { body: 'hi!' }
end
- if can_reply_to_invididual_notes
+ if can_reply_to_individual_notes
it 'creates a new discussion' do
expect(response).to have_gitlab_http_status(201)
expect(json_response['body']).to eq('hi!')
diff --git a/spec/support/shared_examples/requests/api/issues_shared_example_spec.rb b/spec/support/shared_examples/requests/api/issues_shared_example_spec.rb
new file mode 100644
index 00000000000..1133e95e44e
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/issues_shared_example_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+shared_examples 'labeled issues with labels and label_name params' do
+ shared_examples 'returns label names' do
+ it 'returns label names' do
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
+ end
+ end
+
+ shared_examples 'returns basic label entity' do
+ it 'returns basic label entity' do
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels'].pluck('name')).to eq([label_c.title, label_b.title, label.title])
+ expect(json_response.first['labels'].first).to match_schema('/public_api/v4/label_basic')
+ end
+ end
+
+ context 'array of labeled issues when all labels match' do
+ let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}" } }
+
+ it_behaves_like 'returns label names'
+ end
+
+ context 'array of labeled issues when all labels match with labels param as array' do
+ let(:params) { { labels: [label.title, label_b.title, label_c.title] } }
+
+ it_behaves_like 'returns label names'
+ end
+
+ context 'when with_labels_details provided' do
+ context 'array of labeled issues when all labels match' do
+ let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}", with_labels_details: true } }
+
+ it_behaves_like 'returns basic label entity'
+ end
+
+ context 'array of labeled issues when all labels match with labels param as array' do
+ let(:params) { { labels: [label.title, label_b.title, label_c.title], with_labels_details: true } }
+
+ it_behaves_like 'returns basic label entity'
+ end
+ end
+end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index a62830c35f1..6bad5d49b1c 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -12,7 +12,7 @@ class Implementation < GitlabUploader
# user/:id
def dynamic_segment
- File.join(model.class.to_s.underscore, model.id.to_s)
+ File.join(model.class.underscore, model.id.to_s)
end
end
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index 97758f0243e..d9f0e2f3cb7 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -7,33 +7,19 @@ describe PersonalFileUploader do
subject { uploader }
- it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/personal_snippet/\d+],
- upload_path: %r[\h+/\S+],
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet\/\d+\/\h+\/\S+$]
-
- context "object_store is REMOTE" do
+ shared_examples '#base_dir' do
before do
- stub_uploads_object_storage
+ subject.instance_variable_set(:@secret, 'secret')
end
- include_context 'with storage', described_class::Store::REMOTE
-
- it_behaves_like 'builds correct paths',
- store_dir: %r[\d+/\h+],
- upload_path: %r[^personal_snippet\/\d+\/\h+\/<filename>]
- end
-
- describe '#upload_paths' do
- it 'builds correct paths for both local and remote storage' do
- paths = uploader.upload_paths('test.jpg')
+ it 'is prefixed with uploads/-/system' do
+ allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name'))
- expect(paths.first).to match(%r[\h+\/test.jpg])
- expect(paths.second).to match(%r[^personal_snippet\/\d+\/\h+\/test.jpg])
+ expect(described_class.base_dir(model)).to eq("uploads/-/system/personal_snippet/#{model.id}")
end
end
- describe '#to_h' do
+ shared_examples '#to_h' do
before do
subject.instance_variable_set(:@secret, 'secret')
end
@@ -50,6 +36,40 @@ describe PersonalFileUploader do
end
end
+ describe '#upload_paths' do
+ it 'builds correct paths for both local and remote storage' do
+ paths = uploader.upload_paths('test.jpg')
+
+ expect(paths.first).to match(%r[\h+\/test.jpg])
+ expect(paths.second).to match(%r[^personal_snippet\/\d+\/\h+\/test.jpg])
+ end
+ end
+
+ context 'object_store is LOCAL' do
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/personal_snippet/\d+/\h+],
+ upload_path: %r[\h+/\S+],
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet\/\d+\/\h+\/\S+$]
+
+ it_behaves_like '#base_dir'
+ it_behaves_like '#to_h'
+ end
+
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[\d+/\h+],
+ upload_path: %r[^personal_snippet\/\d+\/\h+\/<filename>]
+
+ it_behaves_like '#base_dir'
+ it_behaves_like '#to_h'
+ end
+
describe "#migrate!" do
before do
uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 3b098320ad7..5bb0173ab89 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -7,6 +7,8 @@ describe 'projects/tree/show' do
let(:repository) { project.repository }
before do
+ stub_feature_flags(vue_file_list: false)
+
assign(:project, project)
assign(:repository, repository)
assign(:lfs_blob_ids, [])
diff --git a/spec/workers/cluster_configure_worker_spec.rb b/spec/workers/cluster_configure_worker_spec.rb
index daf014ac574..975088f3ee6 100644
--- a/spec/workers/cluster_configure_worker_spec.rb
+++ b/spec/workers/cluster_configure_worker_spec.rb
@@ -4,11 +4,6 @@ require 'spec_helper'
describe ClusterConfigureWorker, '#perform' do
let(:worker) { described_class.new }
- let(:ci_preparing_state_enabled) { false }
-
- before do
- stub_feature_flags(ci_preparing_state: ci_preparing_state_enabled)
- end
shared_examples 'configured cluster' do
it 'creates a namespace' do
@@ -33,26 +28,14 @@ describe ClusterConfigureWorker, '#perform' do
context 'when group has a project' do
let!(:project) { create(:project, group: group) }
- it_behaves_like 'configured cluster'
-
- context 'ci_preparing_state feature is enabled' do
- let(:ci_preparing_state_enabled) { true }
-
- it_behaves_like 'unconfigured cluster'
- end
+ it_behaves_like 'unconfigured cluster'
end
context 'when group has project in a sub-group' do
let!(:subgroup) { create(:group, parent: group) }
let!(:project) { create(:project, group: subgroup) }
- it_behaves_like 'configured cluster'
-
- context 'ci_preparing_state feature is enabled' do
- let(:ci_preparing_state_enabled) { true }
-
- it_behaves_like 'unconfigured cluster'
- end
+ it_behaves_like 'unconfigured cluster'
end
end
diff --git a/spec/workers/pages_domain_removal_cron_worker_spec.rb b/spec/workers/pages_domain_removal_cron_worker_spec.rb
index 0e1171e8491..2408ad54189 100644
--- a/spec/workers/pages_domain_removal_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_removal_cron_worker_spec.rb
@@ -9,25 +9,10 @@ describe PagesDomainRemovalCronWorker do
context 'when there is domain which should be removed' do
let!(:domain_for_removal) { create(:pages_domain, :should_be_removed) }
- before do
- stub_feature_flags(remove_disabled_domains: true)
- end
-
it 'removes domain' do
expect { worker.perform }.to change { PagesDomain.count }.by(-1)
expect(PagesDomain.exists?).to eq(false)
end
-
- context 'when domain removal is disabled' do
- before do
- stub_feature_flags(remove_disabled_domains: false)
- end
-
- it 'does not remove pages domain' do
- expect { worker.perform }.not_to change { PagesDomain.count }
- expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present
- end
- end
end
context 'where there is a domain which scheduled for removal in the future' do
diff --git a/yarn.lock b/yarn.lock
index 42b6fc642c4..b33a9d914cc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4524,9 +4524,9 @@ fs.realpath@^1.0.0:
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
fsevents@^1.2.2, fsevents@^1.2.3:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
- integrity sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==
+ version "1.2.9"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f"
+ integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==
dependencies:
nan "^2.9.2"
node-pre-gyp "^0.10.0"
@@ -6731,17 +6731,12 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
-lodash.assign@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
- integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=
-
lodash.camelcase@4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
-lodash.clonedeep@^4.3.2, lodash.clonedeep@^4.5.0:
+lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
@@ -7310,10 +7305,10 @@ mute-stream@0.0.7:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
-nan@^2.10.0, nan@^2.9.2:
- version "2.13.1"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.1.tgz#a15bee3790bde247e8f38f1d446edcdaeb05f2dd"
- integrity sha512-I6YB/YEuDeUZMmhscXKxGgZlFnhsn5y0hgOZBadkzfTRrZBtJDZeg6eQf7PYMIEclwmorTKK8GztsyOUSVBREA==
+nan@^2.13.2, nan@^2.9.2:
+ version "2.13.2"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
+ integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
nanomatch@^1.2.9:
version "1.2.9"
@@ -7465,10 +7460,10 @@ node-releases@^1.1.3:
dependencies:
semver "^5.3.0"
-node-sass@^4.11.0:
- version "4.11.0"
- resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a"
- integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==
+node-sass@^4.12.0:
+ version "4.12.0"
+ resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.12.0.tgz#0914f531932380114a30cc5fa4fa63233a25f017"
+ integrity sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==
dependencies:
async-foreach "^0.1.3"
chalk "^1.1.1"
@@ -7477,12 +7472,10 @@ node-sass@^4.11.0:
get-stdin "^4.0.1"
glob "^7.0.3"
in-publish "^2.0.0"
- lodash.assign "^4.2.0"
- lodash.clonedeep "^4.3.2"
- lodash.mergewith "^4.6.0"
+ lodash "^4.17.11"
meow "^3.7.0"
mkdirp "^0.5.1"
- nan "^2.10.0"
+ nan "^2.13.2"
node-gyp "^3.8.0"
npmlog "^4.0.0"
request "^2.88.0"