summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Cobb <rcobb@gitlab.com>2019-05-20 13:12:12 -0600
committerRyan Cobb <rcobb@gitlab.com>2019-05-20 13:12:12 -0600
commit06c3a4b733613dba33ae36ad99d977103f0843aa (patch)
treeb2cc0966aa52b53d8bfcfb211d44d0ef9f0ed5ef
parent618dd80c7d8ebcdb573977780f29bf1475efa4f2 (diff)
parentbdc2eb33e160006cd34128e391becff86a3bc060 (diff)
downloadgitlab-ce-61964-sys-proctable-performance.tar.gz
Merge branch 'master' into 61964-sys-proctable-performance61964-sys-proctable-performance
-rw-r--r--.codeclimate.yml58
-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--Gemfile2
-rw-r--r--Gemfile.lock8
-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/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/lib/utils/highlight.js6
-rw-r--r--app/assets/javascripts/main.js2
-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/javascripts/vue_merge_request_widget/components/deployment.vue37
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue38
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue2
-rw-r--r--app/assets/stylesheets/components/toast.scss54
-rw-r--r--app/assets/stylesheets/framework/common.scss3
-rw-r--r--app/assets/stylesheets/framework/variables.scss11
-rw-r--r--app/assets/stylesheets/pages/members.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss2
-rw-r--r--app/controllers/projects/environments_controller.rb1
-rw-r--r--app/graphql/resolvers/issues_resolver.rb2
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/clusters/applications/jupyter.rb2
-rw-r--r--app/serializers/test_case_entity.rb1
-rw-r--r--app/services/ci/register_job_service.rb5
-rw-r--r--app/services/git/base_hooks_service.rb9
-rw-r--r--app/services/git/branch_push_service.rb1
-rw-r--r--app/views/admin/users/_head.html.haml1
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/projects/_files.html.haml10
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/workers/pages_domain_removal_cron_worker.rb2
-rw-r--r--changelogs/unreleased/1340-request-job-with-age.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/59105-padding-unclickable-pipeline-job.yml5
-rw-r--r--changelogs/unreleased/60818_yamllint_project_root.yml5
-rw-r--r--changelogs/unreleased/61795-fix-error-when-moving-issues.yml5
-rw-r--r--changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml5
-rw-r--r--changelogs/unreleased/display-junit-classname-in-modal.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/kinolaev-master-patch-13154.yml5
-rw-r--r--changelogs/unreleased/sh-fix-tag-push-remote-mirror.yml5
-rw-r--r--changelogs/unreleased/update-gitaly-to-v1-42-1.yml5
-rw-r--r--config/environments/test.rb2
-rw-r--r--config/initializers/action_dispatch_http_mime_negotiation.rb2
-rw-r--r--config/puma.rb.example70
-rw-r--r--db/migrate/20170330141723_disable_invalid_service_templates2.rb2
-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/schema.rb3
-rw-r--r--doc/README.md115
-rw-r--r--doc/administration/dependency_proxy.md150
-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/scim.md235
-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/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_phoenix_app_with_gitlab_ci_cd/img/job-succeeded.pngbin23148 -> 0 bytes
-rw-r--r--doc/ci/merge_request_pipelines/img/pipeline_detail.pngbin15556 -> 0 bytes
-rw-r--r--doc/ci/merge_request_pipelines/index.md15
-rw-r--r--doc/ci/runners/shared_to_specific_admin.pngbin5715 -> 0 bytes
-rw-r--r--doc/ci/services/mysql.md25
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/README.md5
-rw-r--r--doc/development/architecture.md592
-rw-r--r--doc/development/documentation/index.md3
-rw-r--r--doc/development/ee_features.md2
-rw-r--r--doc/development/feature_flags.md8
-rw-r--r--doc/development/gitlab_architecture_diagram.pngbin34253 -> 0 bytes
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/development/rolling_out_changes_using_feature_flags.md11
-rw-r--r--doc/development/testing_guide/best_practices.md30
-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.md48
-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/img/google_app.pngbin18853 -> 0 bytes
-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/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/user/admin_area/img/license_no_license_message.pngbin5778 -> 0 bytes
-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/dependency_scanning/index.md3
-rw-r--r--doc/user/group/clusters/index.md8
-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/instance/clusters/index.md23
-rw-r--r--doc/user/project/clusters/index.md17
-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/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/index.md8
-rw-r--r--doc/user/project/merge_requests/merge_request_approvals.md5
-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--doc/workflow/timezone.md26
-rw-r--r--fixtures/emojis/index.json2
-rw-r--r--lib/api/entities.rb19
-rw-r--r--lib/api/groups.rb24
-rw-r--r--lib/api/helpers/groups_helpers.rb9
-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/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.yml4
-rw-r--r--lib/gitlab/config/entry/validators.rb6
-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/metrics/samplers/ruby_sampler.rb31
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb31
-rw-r--r--lib/gitlab/metrics/system.rb11
-rw-r--r--lib/gitlab/routing.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/lint.rake35
-rw-r--r--locale/gitlab.pot9
-rw-r--r--qa/docs/BEST_PRACTICES.md2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb2
-rw-r--r--qa/spec/spec_helper.rb8
-rw-r--r--scripts/frontend/stylelint/stylelint-utility-map.js2
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb22
-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/lib/utils/text_utility_spec.js4
-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/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js37
-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/import_export/project_tree_saver_spec.rb7
-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/requests/api/discussions_spec.rb4
-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/serializers/test_case_entity_spec.rb2
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb2
-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/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/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/workers/pages_domain_removal_cron_worker_spec.rb15
394 files changed, 8810 insertions, 3078 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/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/Gemfile b/Gemfile
index f33673a14dd..0b9e5000c7c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -169,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'
diff --git a/Gemfile.lock b/Gemfile.lock
index ca6e13b0876..be722b89a40 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -192,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)
@@ -266,8 +266,8 @@ 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)
@@ -1062,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)
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/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/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 1af21715551..9f30a989295 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -148,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');
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/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index da0a9483f8e..8f4cae8ae58 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -23,6 +23,8 @@ export default {
TooltipOnTruncate,
FilteredSearchDropdown,
ReviewAppLink,
+ VisualReviewAppLink: () =>
+ import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -37,6 +39,20 @@ export default {
type: Boolean,
required: true,
},
+ showVisualReviewApp: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ visualReviewAppMeta: {
+ type: Object,
+ required: false,
+ default: () => ({
+ sourceProjectId: '',
+ issueId: '',
+ appUrl: '',
+ }),
+ },
},
deployedTextMap: {
running: __('Deploying to'),
@@ -168,6 +184,11 @@ export default {
:link="deploymentExternalUrl"
:css-class="`deploy-link js-deploy-url inline ${slotProps.className}`"
/>
+ <visual-review-app-link
+ v-if="showVisualReviewApp"
+ :link="deploymentExternalUrl"
+ :app-metadata="visualReviewAppMeta"
+ />
</template>
<template slot="result" slot-scope="slotProps">
@@ -187,11 +208,17 @@ export default {
</a>
</template>
</filtered-search-dropdown>
- <review-app-link
- v-else
- :link="deploymentExternalUrl"
- css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin"
- />
+ <template v-else>
+ <review-app-link
+ :link="deploymentExternalUrl"
+ css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inline"
+ />
+ <visual-review-app-link
+ v-if="showVisualReviewApp"
+ :link="deploymentExternalUrl"
+ :app-metadata="visualReviewAppMeta"
+ />
+ </template>
</template>
<span
v-if="deployment.stop_url"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 5f5fe67b3c1..b9f5f602117 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -30,9 +30,6 @@ export default {
},
},
computed: {
- pipeline() {
- return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
- },
branch() {
return this.isPostMerge ? this.mr.targetBranch : this.mr.sourceBranch;
},
@@ -48,6 +45,19 @@ export default {
hasDeploymentMetrics() {
return this.isPostMerge;
},
+ visualReviewAppMeta() {
+ return {
+ appUrl: this.mr.appUrl,
+ issueId: this.mr.iid,
+ sourceProjectId: this.mr.sourceProjectId,
+ };
+ },
+ pipeline() {
+ return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
+ },
+ showVisualReviewAppLink() {
+ return !!(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable);
+ },
},
};
</script>
@@ -61,14 +71,18 @@ export default {
:source-branch-link="branchLink"
:troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
- <div v-if="deployments.length" slot="footer" class="mr-widget-extension">
- <deployment
- v-for="deployment in deployments"
- :key="deployment.id"
- :class="deploymentClass"
- :deployment="deployment"
- :show-metrics="hasDeploymentMetrics"
- />
- </div>
+ <template v-slot:footer>
+ <div v-if="deployments.length" class="mr-widget-extension">
+ <deployment
+ v-for="deployment in deployments"
+ :key="deployment.id"
+ :class="deploymentClass"
+ :deployment="deployment"
+ :show-metrics="hasDeploymentMetrics"
+ :show-visual-review-app="true"
+ :visual-review-app-meta="visualReviewAppMeta"
+ />
+ </div>
+ </template>
</mr-widget-container>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
index de9c122f268..457a71cab95 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
@@ -19,6 +19,6 @@ export default {
</script>
<template>
<a :href="link" target="_blank" rel="noopener noreferrer nofollow" :class="cssClass">
- {{ __('View app') }} <icon name="external-link" />
+ {{ __('View app') }} <icon css-classes="fgray" name="external-link" />
</a>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index 671b4909839..a620f560b52 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -7,7 +7,7 @@
*
* @example
* <clipboard-button
- * title="Copy to clipbard"
+ * title="Copy to clipboard"
* text="Content to be copied"
* css-class="btn-transparent"
* />
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/common.scss b/app/assets/stylesheets/framework/common.scss
index 2e38b260f5f..fc488b85138 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -5,6 +5,9 @@
.cgreen { color: $green-600; }
.cdark { color: $common-gray-dark; }
+.fwhite { fill: $white-light; }
+.fgray { fill: $gray-700; }
+
.text-plain,
.text-plain:hover {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 9c6c42c3990..1cf122102cc 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -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/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 709940ba6c8..44b558dd5ff 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -972,10 +972,6 @@
}
}
- .btn svg {
- fill: $gray-700;
- }
-
.dropdown-menu {
width: 400px;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 12a3b8c88f3..aa6bbc8e473 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -903,7 +903,7 @@ button.mini-pipeline-graph-dropdown-toggle {
// Match dropdown.scss for all `a` tags
&.non-details-job-component {
- padding: 8px 16px;
+ padding: $gl-padding-8 $gl-btn-horz-padding;
}
.ci-job-name-component {
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/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/models/ci/build.rb b/app/models/ci/build.rb
index 0a90995f689..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
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/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/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/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/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 1c1d1ea4645..f8b7d0c530a 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -18,7 +18,7 @@
%span.logo-text.d-none.d-lg-block.prepend-left-8
= logo_text
- if Gitlab.com?
- = link_to 'https://next.gitlab.com', class: 'label-link js-canary-badge canary-badge bg-transparent', target: :_blank do
+ = 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')
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/_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/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/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/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/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/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/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/59105-padding-unclickable-pipeline-job.yml b/changelogs/unreleased/59105-padding-unclickable-pipeline-job.yml
new file mode 100644
index 00000000000..95f08af3cb1
--- /dev/null
+++ b/changelogs/unreleased/59105-padding-unclickable-pipeline-job.yml
@@ -0,0 +1,5 @@
+---
+title: Fix padding of unclickable pipeline dropdown items to match links
+merge_request:
+author:
+type: fixed
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/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/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/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/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/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/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/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/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/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/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/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/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/schema.rb b/db/schema.rb
index 9d367938cec..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: 20190515125613) do
+ActiveRecord::Schema.define(version: 20190516011213) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -380,6 +380,7 @@ ActiveRecord::Schema.define(version: 20190515125613) 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/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/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/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/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_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/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..b3ff55daea2 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -70,15 +70,18 @@ when a merge request was created or updated. For example:
## Pipelines for Merged Results **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7380) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.10.
+> This feature is disabled by default until we resolve issues with [contention handling](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186), but [can be enabled manually](#enabling-pipelines-for-merged-results).
It's possible for your source and target branches to diverge, which can result
in the scenario that source branch's pipeline was green, the target's pipeline was green,
-but the combined output fails. By having your merge request pipeline automatically
+but the combined output fails.
+
+By having your merge request pipeline automatically
create a new ref that contains the merge result of the source and target branch
(then running a pipeline on that ref), we can better test that the combined result
is also valid.
-From GitLab 11.10, pipelines for merge requests run by default
+GitLab can run pipelines for merge requests
on this merged result. That is, where the source and target branches are combined into a
new ref and a pipeline for this ref validates the result prior to merging.
@@ -95,7 +98,7 @@ get out of WIP status or resolve merge conflicts as soon as possible.
### Enabling Pipelines for Merged Results
-This feature disabled by default until we resolve issues with [contention handling](https://gitlab.com/gitlab-org/gitlab-ee/issues/9186). It can be enabled at the project level:
+To enable pipelines on merged results at the project level:
1. Visit your project's **Settings > General** and expand **Merge requests**.
1. Check **Merge pipelines will try to validate the post-merge result prior to merging**.
@@ -103,6 +106,10 @@ This feature disabled by default until we resolve issues with [contention handli
![Merge request pipeline config](img/merge_request_pipeline_config.png)
+CAUTION: **Warning:**
+Make sure your `gitlab-ci.yml` file is [configured properly for pipelines for merge requests](#configuring-pipelines-for-merge-requests),
+otherwise pipelines for merged results won't run and your merge requests will be stuck in an unresolved state.
+
### Pipelines for Merged Result's limitations
- This feature requires [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer.
@@ -193,5 +200,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 0de6a93514a..5fa378fc4c2 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -18,7 +18,7 @@ services:
variables:
# Configure mysql environment variables (https://hub.docker.com/_/mysql/)
MYSQL_DATABASE: "<your_mysql_database>"
- MYSQL_ROOT_PASSWORD: "mysql_strong_password"
+ 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 36b88ff12c2..31ff56e06f8 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1510,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.
diff --git a/doc/development/README.md b/doc/development/README.md
index cfbbd5aaeaa..2ff38d68a47 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -137,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 56c5056fd6d..9a012f4299b 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -10,13 +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.
+
+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/).
+
+### 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
+```
+
+### 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
+**Layers**
GitLab can be considered to have two layers from a process perspective:
@@ -25,100 +159,197 @@ 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.
-### GitLab Process Descriptions
+#### Alertmanager
-As of this writing, a fresh GitLab 11.3.0 install with default settings will show the following processes with `gitlab-ctl status`:
+- [Project page](https://github.com/prometheus/alertmanager/blob/master/README.md)
+- Configuration: [Omnibus][alertmanager-omnibus], [Charts][alertmanager-charts]
+- Layer: Monitoring
-```
-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
-```
+[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.
-#### alertmanager
+#### Certificate management
-- Omnibus configuration options
-- Layer: Monitoring
+- 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)
-[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.
+#### 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.
-#### gitaly
+#### Database migrations
-- [Omnibus configuration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration)
+- 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
+
+- [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).
-#### gitlab-monitor
+#### Gitlab Geo
+
+- Configuration: [Omnibus][geo-omnibus], [Charts][geo-charts]
+- Layer: Core Service (Processor)
-- Omnibus configuration options
+#### Gitlab Monitor
+
+- [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-workhorse
+#### Gitlab Pages
-- Omnibus configuration options
+- Configuration: [Omnibus][pages-omnibus], [Charts][pages-charts]
+- Layer: Core Service (Processor)
+
+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)
+
+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)
[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.
-#### logrotate
+#### 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.
-- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate)
+#### 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 open source offering.
-#### nginx
+#### 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
-- [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.
-#### postgres-exporter
+#### PgBouncer
+
+- [Project page](https://github.com/pgbouncer/pgbouncer/blob/master/README.md)
+- Configuration: [Omnibus][pgbouncer-omnibus], [Charts][pgbouncer-charts]
+- Layer: Core Service (Data)
-- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html)
+Lightweight connection pooler for PostgreSQL.
+
+#### 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:
@@ -127,50 +358,75 @@ Redis is packaged to provide a place to store:
- temporary cache information
- 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 Grafana.
-#### sidekiq
+#### Registry
+
+- [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
-- Omnibus configuration options
+- [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
-The following processes will be running if corresponding feature is enabled.
+- Configuration: [Omnibus][ldap-omnibus], [Charts][ldap-charts]
+- Layer: Core Service (Processor)
-##### gitlab-pages
+#### Outbound Email
-TODO
+- Configuration: [Omnibus][outbound-email-omnibus], [Charts][outbound-email-charts]
+- Layer: Core Service (Processor)
-##### mattermost
+#### Inbound Email
-TODO
+- Configuration: [Omnibus][inbound-email-omnibus], [Charts][inbound-email-charts]
+- Layer: Core Service (Processor)
-##### registry
+#### Kubernetes Cluster Apps
-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/).
+- Configuration: [Omnibus][managed-k8s-apps], [Charts][managed-k8s-apps]
+- Layer: Core Service (Processor)
-An external registry can also be configured to use GitLab as an auth endpoint.
+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
@@ -210,140 +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
-
-#### 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
-
-```
-
-#### Component Legend
-
-* ✅ - Automatically configured
-* ⚙ - Requires additional configuration
-* ⤓ - Additional software/service required
-* ❌ - Not available
-
-#### Component Table
-
-| 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 |
-
-#### Component Overview
-
-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).
@@ -470,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 5633c8313a3..4bf8401c0e8 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -96,10 +96,9 @@ 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:
+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))
-- `doc/README.md`
Eventually, once all docs are aligned, we'll remove any doc reference from that
script, so it catches everything.
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 9db28bcddf8..bc472bb5b0a 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -910,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/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/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index ac04a21b37a..faea8d5982f 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -58,6 +58,7 @@ are very appreciative of the work done by translators and proofreaders!
- Japanese
- Yamana Tokiuji - [GitLab](https://gitlab.com/tokiuji), [Crowdin](https://crowdin.com/profile/yamana)
- Hiroyuki Sato - [GitLab](https://gitlab.com/hiroponz), [Crowdin](https://crowdin.com/profile/hiroponz)
+ - Tomo Dote - [Gitlab](https://gitlab.com/fu7mu4), [Crowdin](https://crowdin.com/profile/fu7mu4)
- Korean
- Chang-Ho Cha - [GitLab](https://gitlab.com/changho-cha), [Crowdin](https://crowdin.com/profile/zzazang)
- Ji Hun Oh - [GitLab](https://gitlab.com/Baw-Appie), [Crowdin](https://crowdin.com/profile/BawAppie)
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/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/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 1fff3e92280..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.
@@ -875,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/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/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/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/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/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/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/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 2d0c2be4233..5d2bb4e572b 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -19,8 +19,9 @@ merge request.
![Dependency Scanning Widget](img/dependency_scanning.png)
-The results are sorted by the priority of the vulnerability:
+The results are sorted by the severity of the vulnerability:
+1. Critical
1. High
1. Medium
1. Low
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
index 53c82169e15..ff6aa4f5930 100644
--- a/doc/user/group/clusters/index.md
+++ b/doc/user/group/clusters/index.md
@@ -5,10 +5,10 @@
## Overview
-Similar to [project Kubernetes
-clusters](../../project/clusters/index.md), Group-level Kubernetes
-clusters allow you to connect a Kubernetes cluster to your group,
-enabling you to use the same cluster across multiple projects.
+Similar to [project-level](../../project/clusters/index.md) and
+[instance-level](../../instance/clusters/index.md) Kubernetes clusters,
+group-level Kubernetes clusters allow you to connect a Kubernetes cluster to
+your group, enabling you to use the same cluster across multiple projects.
## Installing applications
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/instance/clusters/index.md b/doc/user/instance/clusters/index.md
new file mode 100644
index 00000000000..894f83d3c75
--- /dev/null
+++ b/doc/user/instance/clusters/index.md
@@ -0,0 +1,23 @@
+# Instance-level Kubernetes clusters
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/39840) in GitLab 11.11.
+> Instance-level cluster integration is currently in [Beta](https://about.gitlab.com/handbook/product/#alpha-beta-ga).
+
+## Overview
+
+Similar to [project-level](../../project/clusters/index.md)
+and [group-level](../../group/clusters/index.md) Kubernetes clusters,
+instance-level Kubernetes clusters allow you to connect a Kubernetes cluster to
+the GitLab instance, which enables you to use the same cluster across multiple
+projects.
+
+## Cluster precedence
+
+GitLab will try match to clusters in the following order:
+
+- Project-level clusters
+- Group-level clusters
+- Instance level
+
+To be selected, the cluster must be enabled and
+match the [environment selector](../../../ci/environments.md#scoping-environments-with-specs-premium).
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 157afb3a78c..e60317bc199 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -19,8 +19,10 @@ or provide the credentials to an [existing Kubernetes cluster](#adding-an-existi
NOTE: **Note:**
From [GitLab 11.6](https://gitlab.com/gitlab-org/gitlab-ce/issues/34758) you
-can also associate a Kubernetes cluster to your groups. Learn more about
-[group Kubernetes clusters](../../group/clusters/index.md).
+can also associate a Kubernetes cluster to your groups and from
+[GitLab 11.11](https://gitlab.com/gitlab-org/gitlab-ce/issues/39840),
+to the GitLab instance. Learn more about [group-level](../../group/clusters/index.md)
+and [instance-level](../../instance/clusters/index.md) Kubernetes clusters.
## Adding and creating a new GKE cluster via GitLab
@@ -674,11 +676,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 +698,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/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/index.md b/doc/user/project/merge_requests/index.md
index 2bb2d906453..723195224d0 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -397,6 +397,14 @@ GitLab can scan and report any vulnerabilities found in your project.
[Read more about security reports.](https://docs.gitlab.com/ee/user/application_security/index.html)
+## JUnit test reports
+
+Configure your CI jobs to use JUnit test reports, and let GitLab display a report
+on the merge request so that it’s easier and faster to identify the failure
+without having to check the entire job log.
+
+[Read more about JUnit test reports](../../../ci/junit_test_reports.md).
+
## Live preview with Review Apps
If you configured [Review Apps](https://about.gitlab.com/features/review-apps/) for your project,
diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md
index ea86633d9e2..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
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/doc/workflow/timezone.md b/doc/workflow/timezone.md
index 338b3a32265..da51c0f2c93 100644
--- a/doc/workflow/timezone.md
+++ b/doc/workflow/timezone.md
@@ -1,31 +1,39 @@
# Changing your time zone
The global time zone configuration parameter can be changed in `config/gitlab.yml`:
+
```
- # time_zone: 'UTC'
+# time_zone: 'UTC'
```
-Uncomment and customize if you want to change the default time zone of GitLab application.
+Uncomment and customize if you want to change the default time zone of the GitLab application.
+
+
+## Viewing available timezones
To see all available time zones, run `bundle exec rake time:zones:all`.
-With Omnibus installations, run `gitlab-rake time:zones:all`.
+For Omnibus installations, run `gitlab-rake time:zones:all`.
+
+NOTE: **Note:**
+Currently, this rake task does not list timezones in TZInfo format required by GitLab Omnibus during a reconfigure: [#58672](https://gitlab.com/gitlab-org/gitlab-ce/issues/58672).
## Changing time zone in omnibus installations
GitLab defaults its time zone to UTC. It has a global timezone configuration parameter in `/etc/gitlab/gitlab.rb`.
-To update, add the time zone that best applies to your location. Here are two examples:
+To obtain a list of timezones, log in to your GitLab application server and run a command that generates a list of timezones in TZInfo format for the server. For example, install `timedatectl` and run `timedatectl list-timezones`.
+
+To update, add the timezone that best applies to your location. For example:
+
```
gitlab_rails['time_zone'] = 'America/New_York'
```
-or
-```
-gitlab_rails['time_zone'] = 'Europe/Brussels'
-```
-After you added this field, reconfigure and restart:
+After adding the configuration parameter, reconfigure and restart your GitLab instance:
+
```
gitlab-ctl reconfigure
gitlab-ctl restart
```
+
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 296688ba25b..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))
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index ad16f26f5cc..6893c8c40be 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -20,20 +20,19 @@ module API
optional :share_with_group_lock, type: Boolean, desc: 'Prevent sharing a project with another group within this group'
end
- if Gitlab.ee?
- params :optional_params_ee do
- optional :membership_lock, type: Boolean, desc: 'Prevent adding new members to project membership within this group'
- optional :ldap_cn, type: String, desc: 'LDAP Common Name'
- optional :ldap_access, type: Integer, desc: 'A valid access level'
- optional :shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Pipeline minutes quota for this group'
- optional :extra_shared_runners_minutes_limit, type: Integer, desc: '(admin-only) Extra pipeline minutes quota for this group'
- all_or_none_of :ldap_cn, :ldap_access
- end
+ params :optional_params_ee do
+ end
+
+ params :optional_update_params_ee do
end
+ end
+ include ::API::Helpers::GroupsHelpers
+
+ helpers do
params :optional_params do
use :optional_params_ce
- use :optional_params_ee if Gitlab.ee?
+ use :optional_params_ee
end
params :statistics_params do
@@ -176,10 +175,7 @@ module API
optional :name, type: String, desc: 'The name of the group'
optional :path, type: String, desc: 'The path of the group'
use :optional_params
-
- if Gitlab.ee?
- optional :file_template_project_id, type: Integer, desc: 'The ID of a project to use for custom templates in this group'
- end
+ use :optional_update_params_ee
end
put ':id' do
group = find_group!(params[:id])
diff --git a/lib/api/helpers/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb
new file mode 100644
index 00000000000..ae677547760
--- /dev/null
+++ b/lib/api/helpers/groups_helpers.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module GroupsHelpers
+ extend ActiveSupport::Concern
+ end
+ end
+end
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/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 876f53c66ba..5f7af2ffc20 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -439,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/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/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/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 1b7a4c79080..da9023676cf 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,13 @@ module Gitlab
def unicorn_with_listeners?
defined?(Unicorn) && Unicorn.listener_names.any?
end
+<<<<<<< HEAD
def unicorn_workers_count
`pgrep -f '[u]nicorn_rails worker.+ #{Rails.root.to_s}'`.split.count
end
+=======
+>>>>>>> master
end
end
end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index d3cbd200584..eeb7858e002 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -23,6 +23,7 @@ module Gitlab
def self.file_descriptor_count
Dir.glob('/proc/self/fd/*').length
end
+<<<<<<< HEAD
def self.max_open_file_descriptors
match = File.read('/proc/self/limits').match(/Max open files\s*(\d+)/)
@@ -37,6 +38,8 @@ module Gitlab
( fields[21].to_i || 0 ) / clk_tck
end
+=======
+>>>>>>> master
else
def self.memory_usage
0.0
@@ -45,14 +48,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/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/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/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 5800aa9604b..c20cae9665a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6058,6 +6058,9 @@ msgstr ""
msgid "Mirroring settings were successfully updated."
msgstr ""
+msgid "Missing commit signatures endpoint!"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
@@ -8065,6 +8068,9 @@ msgstr ""
msgid "Reports|Class"
msgstr ""
+msgid "Reports|Classname"
+msgstr ""
+
msgid "Reports|Execution time"
msgstr ""
@@ -8376,6 +8382,9 @@ msgstr ""
msgid "Search files"
msgstr ""
+msgid "Search for a group"
+msgstr ""
+
msgid "Search for projects, issues, etc."
msgstr ""
diff --git a/qa/docs/BEST_PRACTICES.md b/qa/docs/BEST_PRACTICES.md
index a872b915926..3a2640607e4 100644
--- a/qa/docs/BEST_PRACTICES.md
+++ b/qa/docs/BEST_PRACTICES.md
@@ -6,7 +6,7 @@ A good example is a user being logged in as a pre-condition for testing the feat
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 successfull login through the GUI takes 2 seconds.
+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".
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/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/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/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/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/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/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/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
index e5155573f6f..dfbc68c48b9 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { mount, createLocalVue } from '@vue/test-utils';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import { mockStore } from '../mock_data';
@@ -9,7 +9,7 @@ describe('MrWidgetPipelineContainer', () => {
const factory = (props = {}) => {
const localVue = createLocalVue();
- wrapper = shallowMount(localVue.extend(MrWidgetPipelineContainer), {
+ wrapper = mount(localVue.extend(MrWidgetPipelineContainer), {
propsData: {
mr: Object.assign({}, mockStore),
...props,
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index dda16375103..bec16b0aab0 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -235,11 +235,44 @@ export default {
troubleshooting_docs_path: 'help',
merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md',
squash: true,
+ visual_review_app_available: true,
};
export const mockStore = {
- pipeline: { id: 0 },
- mergePipeline: { id: 1 },
+ pipeline: {
+ id: 0,
+ details: {
+ status: {
+ details_path: '/root/review-app-tester/pipelines/66',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2. png',
+ group: 'success-with-warnings',
+ has_details: true,
+ icon: 'status_warning',
+ illustration: null,
+ label: 'passed with warnings',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ },
+ },
+ mergePipeline: {
+ id: 1,
+ details: {
+ status: {
+ details_path: '/root/review-app-tester/pipelines/66',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2. png',
+ group: 'success-with-warnings',
+ has_details: true,
+ icon: 'status_warning',
+ illustration: null,
+ label: 'passed with warnings',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ },
+ },
targetBranch: 'target-branch',
sourceBranch: 'source-branch',
sourceBranchLink: 'source-branch-link',
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/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/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/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/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/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/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/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/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/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